﻿/**
 * @license wysihtml5 v0.3.0 https://github.com/xing/wysihtml5
 * 
 * Author: Christopher Blum (https://github.com/tiff)
 * 
 * Copyright (C) 2012 XING AG Licensed under the MIT license (MIT)
 * 
 */
var wysihtml5 = {
	version : "0.3.0",

	// namespaces
	commands : {},
	dom : {},
	quirks : {},
	toolbar : {},
	lang : {},
	selection : {},
	views : {},

	INVISIBLE_SPACE : "\uFEFF",

	EMPTY_FUNCTION : function() {
	},

	ELEMENT_NODE : 1,
	TEXT_NODE : 3,

	BACKSPACE_KEY : 8,
	ENTER_KEY : 13,
	ESCAPE_KEY : 27,
	SPACE_KEY : 32,
	DELETE_KEY : 46
};
/**
 * @license Rangy, a cross-browser JavaScript range and selection library
 *          http://code.google.com/p/rangy/
 * 
 * Copyright 2011, Tim Down Licensed under the MIT license. Version: 1.2.2 Build
 * date: 13 November 2011
 */
window['rangy'] = (function() {

	var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";

	var domRangeProperties = [ "startContainer", "startOffset", "endContainer",
			"endOffset", "collapsed", "commonAncestorContainer",
			"START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END" ];

	var domRangeMethods = [ "setStart", "setStartBefore", "setStartAfter",
			"setEnd", "setEndBefore", "setEndAfter", "collapse", "selectNode",
			"selectNodeContents", "compareBoundaryPoints", "deleteContents",
			"extractContents", "cloneContents", "insertNode",
			"surroundContents", "cloneRange", "toString", "detach" ];

	var textRangeProperties = [ "boundingHeight", "boundingLeft",
			"boundingTop", "boundingWidth", "htmlText", "text" ];

	// Subset of TextRange's full set of methods that we're interested in
	var textRangeMethods = [ "collapse", "compareEndPoints", "duplicate",
			"getBookmark", "moveToBookmark", "moveToElementText",
			"parentElement", "pasteHTML", "select", "setEndPoint",
			"getBoundingClientRect" ];

	/*----------------------------------------------------------------------------------------------------------------*/

	// Trio of functions taken from Peter Michaux's article:
	// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
	function isHostMethod(o, p) {
		var t = typeof o[p];
		return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
	}

	function isHostObject(o, p) {
		return !!(typeof o[p] == OBJECT && o[p]);
	}

	function isHostProperty(o, p) {
		return typeof o[p] != UNDEFINED;
	}

	// Creates a convenience function to save verbose repeated calls to tests
	// functions
	function createMultiplePropertyTest(testFunc) {
		return function(o, props) {
			var i = props.length;
			while (i--) {
				if (!testFunc(o, props[i])) {
					return false;
				}
			}
			return true;
		};
	}

	// Next trio of functions are a convenience to save verbose repeated calls
	// to previous two functions
	var areHostMethods = createMultiplePropertyTest(isHostMethod);
	var areHostObjects = createMultiplePropertyTest(isHostObject);
	var areHostProperties = createMultiplePropertyTest(isHostProperty);

	function isTextRange(range) {
		return range && areHostMethods(range, textRangeMethods)
				&& areHostProperties(range, textRangeProperties);
	}

	var api = {
		version : "1.2.2",
		initialized : false,
		supported : true,

		util : {
			isHostMethod : isHostMethod,
			isHostObject : isHostObject,
			isHostProperty : isHostProperty,
			areHostMethods : areHostMethods,
			areHostObjects : areHostObjects,
			areHostProperties : areHostProperties,
			isTextRange : isTextRange
		},

		features : {},

		modules : {},
		config : {
			alertOnWarn : false,
			preferTextRange : false
		}
	};

	function fail(reason) {
		window.alert("Rangy not supported in your browser. Reason: " + reason);
		api.initialized = true;
		api.supported = false;
	}

	api.fail = fail;

	function warn(msg) {
		var warningMessage = "Rangy warning: " + msg;
		if (api.config.alertOnWarn) {
			window.alert(warningMessage);
		} else if (typeof window.console != UNDEFINED
				&& typeof window.console.log != UNDEFINED) {
			window.console.log(warningMessage);
		}
	}

	api.warn = warn;

	if ({}.hasOwnProperty) {
		api.util.extend = function(o, props) {
			for ( var i in props) {
				if (props.hasOwnProperty(i)) {
					o[i] = props[i];
				}
			}
		};
	} else {
		fail("hasOwnProperty not supported");
	}

	var initListeners = [];
	var moduleInitializers = [];

	// Initialization
	function init() {
		if (api.initialized) {
			return;
		}
		var testRange;
		var implementsDomRange = false, implementsTextRange = false;

		// First, perform basic feature tests

		if (isHostMethod(document, "createRange")) {
			testRange = document.createRange();
			if (areHostMethods(testRange, domRangeMethods)
					&& areHostProperties(testRange, domRangeProperties)) {
				implementsDomRange = true;
			}
			testRange.detach();
		}

		var body = isHostObject(document, "body") ? document.body : document
				.getElementsByTagName("body")[0];

		if (body && isHostMethod(body, "createTextRange")) {
			testRange = body.createTextRange();
			if (isTextRange(testRange)) {
				implementsTextRange = true;
			}
		}

		if (!implementsDomRange && !implementsTextRange) {
			fail("Neither Range nor TextRange are implemented");
		}

		api.initialized = true;
		api.features = {
			implementsDomRange : implementsDomRange,
			implementsTextRange : implementsTextRange
		};

		// Initialize modules and call init listeners
		var allListeners = moduleInitializers.concat(initListeners);
		for ( var i = 0, len = allListeners.length; i < len; ++i) {
			try {
				allListeners[i](api);
			} catch (ex) {
				if (isHostObject(window, "console")
						&& isHostMethod(window.console, "log")) {
					window.console
							.log(
									"Init listener threw an exception. Continuing.",
									ex);
				}

			}
		}
	}

	// Allow external scripts to initialize this library in case it's loaded
	// after the document has loaded
	api.init = init;

	// Execute listener immediately if already initialized
	api.addInitListener = function(listener) {
		if (api.initialized) {
			listener(api);
		} else {
			initListeners.push(listener);
		}
	};

	var createMissingNativeApiListeners = [];

	api.addCreateMissingNativeApiListener = function(listener) {
		createMissingNativeApiListeners.push(listener);
	};

	function createMissingNativeApi(win) {
		win = win || window;
		init();

		// Notify listeners
		for ( var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
			createMissingNativeApiListeners[i](win);
		}
	}

	api.createMissingNativeApi = createMissingNativeApi;

	/**
	 * @constructor
	 */
	function Module(name) {
		this.name = name;
		this.initialized = false;
		this.supported = false;
	}

	Module.prototype.fail = function(reason) {
		this.initialized = true;
		this.supported = false;

		throw new Error("Module '" + this.name + "' failed to load: " + reason);
	};

	Module.prototype.warn = function(msg) {
		api.warn("Module " + this.name + ": " + msg);
	};

	Module.prototype.createError = function(msg) {
		return new Error("Error in Rangy " + this.name + " module: " + msg);
	};

	api.createModule = function(name, initFunc) {
		var module = new Module(name);
		api.modules[name] = module;

		moduleInitializers.push(function(api) {
			initFunc(api, module);
			module.initialized = true;
			module.supported = true;
		});
	};

	api.requireModules = function(modules) {
		for ( var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
			moduleName = modules[i];
			module = api.modules[moduleName];
			if (!module || !(module instanceof Module)) {
				throw new Error("Module '" + moduleName + "' not found");
			}
			if (!module.supported) {
				throw new Error("Module '" + moduleName + "' not supported");
			}
		}
	};

	/*----------------------------------------------------------------------------------------------------------------*/

	// Wait for document to load before running tests
	var docReady = false;

	var loadHandler = function(e) {

		if (!docReady) {
			docReady = true;
			if (!api.initialized) {
				init();
			}
		}
	};

	// Test whether we have window and document objects that we will need
	if (typeof window == UNDEFINED) {
		fail("No window found");
		return;
	}
	if (typeof document == UNDEFINED) {
		fail("No document found");
		return;
	}

	if (isHostMethod(document, "addEventListener")) {
		document.addEventListener("DOMContentLoaded", loadHandler, false);
	}

	// Add a fallback in case the DOMContentLoaded event isn't supported
	if (isHostMethod(window, "addEventListener")) {
		window.addEventListener("load", loadHandler, false);
	} else if (isHostMethod(window, "attachEvent")) {
		window.attachEvent("onload", loadHandler);
	} else {
		fail("Window does not have required addEventListener or attachEvent method");
	}

	return api;
})();
rangy
		.createModule(
				"DomUtil",
				function(api, module) {

					var UNDEF = "undefined";
					var util = api.util;

					// Perform feature tests
					if (!util.areHostMethods(document, [
							"createDocumentFragment", "createElement",
							"createTextNode" ])) {
						module.fail("document missing a Node creation method");
					}

					if (!util.isHostMethod(document, "getElementsByTagName")) {
						module
								.fail("document missing getElementsByTagName method");
					}

					var el = document.createElement("div");
					if (!util.areHostMethods(el, [ "insertBefore",
							"appendChild", "cloneNode" ]
							|| !util
									.areHostObjects(el, [ "previousSibling",
											"nextSibling", "childNodes",
											"parentNode" ]))) {
						module.fail("Incomplete Element implementation");
					}

					// innerHTML is required for Range's
					// createContextualFragment method
					if (!util.isHostProperty(el, "innerHTML")) {
						module.fail("Element is missing innerHTML property");
					}

					var textNode = document.createTextNode("test");
					if (!util.areHostMethods(textNode, [ "splitText",
							"deleteData", "insertData", "appendData",
							"cloneNode" ]
							|| !util
									.areHostObjects(el, [ "previousSibling",
											"nextSibling", "childNodes",
											"parentNode" ])
							|| !util.areHostProperties(textNode, [ "data" ]))) {
						module.fail("Incomplete Text Node implementation");
					}

					/*----------------------------------------------------------------------------------------------------------------*/

					// Removed use of indexOf because of a bizarre bug in Opera
					// that is thrown in one of the Acid3 tests. I haven't been
					// able to replicate it outside of the test. The bug is that
					// indexOf returns -1 when called on an Array that
					// contains just the document as a single element and the
					// value searched for is the document.
					var arrayContains = /*
										 * Array.prototype.indexOf ?
										 * function(arr, val) { return
										 * arr.indexOf(val) > -1; }:
										 */

					function(arr, val) {
						var i = arr.length;
						while (i--) {
							if (arr[i] === val) {
								return true;
							}
						}
						return false;
					};

					// Opera 11 puts HTML elements in the null namespace, it
					// seems, and IE 7 has undefined namespaceURI
					function isHtmlNamespace(node) {
						var ns;
						return typeof node.namespaceURI == UNDEF
								|| ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
					}

					function parentElement(node) {
						var parent = node.parentNode;
						return (parent.nodeType == 1) ? parent : null;
					}

					function getNodeIndex(node) {
						var i = 0;
						while ((node = node.previousSibling)) {
							i++;
						}
						return i;
					}

					function getNodeLength(node) {
						var childNodes;
						return isCharacterDataNode(node) ? node.length
								: ((childNodes = node.childNodes) ? childNodes.length
										: 0);
					}

					function getCommonAncestor(node1, node2) {
						var ancestors = [], n;
						for (n = node1; n; n = n.parentNode) {
							ancestors.push(n);
						}

						for (n = node2; n; n = n.parentNode) {
							if (arrayContains(ancestors, n)) {
								return n;
							}
						}

						return null;
					}

					function isAncestorOf(ancestor, descendant, selfIsAncestor) {
						var n = selfIsAncestor ? descendant
								: descendant.parentNode;
						while (n) {
							if (n === ancestor) {
								return true;
							} else {
								n = n.parentNode;
							}
						}
						return false;
					}

					function getClosestAncestorIn(node, ancestor,
							selfIsAncestor) {
						var p, n = selfIsAncestor ? node : node.parentNode;
						while (n) {
							p = n.parentNode;
							if (p === ancestor) {
								return n;
							}
							n = p;
						}
						return null;
					}

					function isCharacterDataNode(node) {
						var t = node.nodeType;
						return t == 3 || t == 4 || t == 8; // Text,
						// CDataSection or
						// Comment
					}

					function insertAfter(node, precedingNode) {
						var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
						if (nextNode) {
							parent.insertBefore(node, nextNode);
						} else {
							parent.appendChild(node);
						}
						return node;
					}

					// Note that we cannot use splitText() because it is
					// bugridden in IE 9.
					function splitDataNode(node, index) {
						var newNode = node.cloneNode(false);
						newNode.deleteData(0, index);
						node.deleteData(index, node.length - index);
						insertAfter(newNode, node);
						return newNode;
					}

					function getDocument(node) {
						if (node.nodeType == 9) {
							return node;
						} else if (typeof node.ownerDocument != UNDEF) {
							return node.ownerDocument;
						} else if (typeof node.document != UNDEF) {
							return node.document;
						} else if (node.parentNode) {
							return getDocument(node.parentNode);
						} else {
							throw new Error(
									"getDocument: no document found for node");
						}
					}

					function getWindow(node) {
						var doc = getDocument(node);
						if (typeof doc.defaultView != UNDEF) {
							return doc.defaultView;
						} else if (typeof doc.parentWindow != UNDEF) {
							return doc.parentWindow;
						} else {
							throw new Error(
									"Cannot get a window object for node");
						}
					}

					function getIframeDocument(iframeEl) {
						if (typeof iframeEl.contentDocument != UNDEF) {
							return iframeEl.contentDocument;
						} else if (typeof iframeEl.contentWindow != UNDEF) {
							return iframeEl.contentWindow.document;
						} else {
							throw new Error(
									"getIframeWindow: No Document object found for iframe element");
						}
					}

					function getIframeWindow(iframeEl) {
						if (typeof iframeEl.contentWindow != UNDEF) {
							return iframeEl.contentWindow;
						} else if (typeof iframeEl.contentDocument != UNDEF) {
							return iframeEl.contentDocument.defaultView;
						} else {
							throw new Error(
									"getIframeWindow: No Window object found for iframe element");
						}
					}

					function getBody(doc) {
						return util.isHostObject(doc, "body") ? doc.body : doc
								.getElementsByTagName("body")[0];
					}

					function getRootContainer(node) {
						var parent;
						while ((parent = node.parentNode)) {
							node = parent;
						}
						return node;
					}

					function comparePoints(nodeA, offsetA, nodeB, offsetB) {
						// See
						// http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
						var nodeC, root, childA, childB, n;
						if (nodeA == nodeB) {

							// Case 1: nodes are the same
							return offsetA === offsetB ? 0
									: (offsetA < offsetB) ? -1 : 1;
						} else if ((nodeC = getClosestAncestorIn(nodeB, nodeA,
								true))) {

							// Case 2: node C (container B or an ancestor) is a
							// child node of A
							return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
						} else if ((nodeC = getClosestAncestorIn(nodeA, nodeB,
								true))) {

							// Case 3: node C (container A or an ancestor) is a
							// child node of B
							return getNodeIndex(nodeC) < offsetB ? -1 : 1;
						} else {

							// Case 4: containers are siblings or descendants of
							// siblings
							root = getCommonAncestor(nodeA, nodeB);
							childA = (nodeA === root) ? root
									: getClosestAncestorIn(nodeA, root, true);
							childB = (nodeB === root) ? root
									: getClosestAncestorIn(nodeB, root, true);

							if (childA === childB) {
								// This shouldn't be possible

								throw new Error(
										"comparePoints got to case 4 and childA and childB are the same!");
							} else {
								n = root.firstChild;
								while (n) {
									if (n === childA) {
										return -1;
									} else if (n === childB) {
										return 1;
									}
									n = n.nextSibling;
								}
								throw new Error("Should not be here!");
							}
						}
					}

					function fragmentFromNodeChildren(node) {
						var fragment = getDocument(node)
								.createDocumentFragment(), child;
						while ((child = node.firstChild)) {
							fragment.appendChild(child);
						}
						return fragment;
					}

					function inspectNode(node) {
						if (!node) {
							return "[No node]";
						}
						if (isCharacterDataNode(node)) {
							return '"' + node.data + '"';
						} else if (node.nodeType == 1) {
							var idAttr = node.id ? ' id="' + node.id + '"' : "";
							return "<" + node.nodeName + idAttr + ">["
									+ node.childNodes.length + "]";
						} else {
							return node.nodeName;
						}
					}

					/**
					 * @constructor
					 */
					function NodeIterator(root) {
						this.root = root;
						this._next = root;
					}

					NodeIterator.prototype = {
						_current : null,

						hasNext : function() {
							return !!this._next;
						},

						next : function() {
							var n = this._current = this._next;
							var child, next;
							if (this._current) {
								child = n.firstChild;
								if (child) {
									this._next = child;
								} else {
									next = null;
									while ((n !== this.root)
											&& !(next = n.nextSibling)) {
										n = n.parentNode;
									}
									this._next = next;
								}
							}
							return this._current;
						},

						detach : function() {
							this._current = this._next = this.root = null;
						}
					};

					function createIterator(root) {
						return new NodeIterator(root);
					}

					/**
					 * @constructor
					 */
					function DomPosition(node, offset) {
						this.node = node;
						this.offset = offset;
					}

					DomPosition.prototype = {
						equals : function(pos) {
							return this.node === pos.node
									& this.offset == pos.offset;
						},

						inspect : function() {
							return "[DomPosition(" + inspectNode(this.node)
									+ ":" + this.offset + ")]";
						}
					};

					/**
					 * @constructor
					 */
					function DOMException(codeName) {
						this.code = this[codeName];
						this.codeName = codeName;
						this.message = "DOMException: " + this.codeName;
					}

					DOMException.prototype = {
						INDEX_SIZE_ERR : 1,
						HIERARCHY_REQUEST_ERR : 3,
						WRONG_DOCUMENT_ERR : 4,
						NO_MODIFICATION_ALLOWED_ERR : 7,
						NOT_FOUND_ERR : 8,
						NOT_SUPPORTED_ERR : 9,
						INVALID_STATE_ERR : 11
					};

					DOMException.prototype.toString = function() {
						return this.message;
					};

					api.dom = {
						arrayContains : arrayContains,
						isHtmlNamespace : isHtmlNamespace,
						parentElement : parentElement,
						getNodeIndex : getNodeIndex,
						getNodeLength : getNodeLength,
						getCommonAncestor : getCommonAncestor,
						isAncestorOf : isAncestorOf,
						getClosestAncestorIn : getClosestAncestorIn,
						isCharacterDataNode : isCharacterDataNode,
						insertAfter : insertAfter,
						splitDataNode : splitDataNode,
						getDocument : getDocument,
						getWindow : getWindow,
						getIframeWindow : getIframeWindow,
						getIframeDocument : getIframeDocument,
						getBody : getBody,
						getRootContainer : getRootContainer,
						comparePoints : comparePoints,
						inspectNode : inspectNode,
						fragmentFromNodeChildren : fragmentFromNodeChildren,
						createIterator : createIterator,
						DomPosition : DomPosition
					};

					api.DOMException = DOMException;
				});
rangy
		.createModule(
				"DomRange",
				function(api, module) {
					api.requireModules([ "DomUtil" ]);

					var dom = api.dom;
					var DomPosition = dom.DomPosition;
					var DOMException = api.DOMException;

					/*----------------------------------------------------------------------------------------------------------------*/

					// Utility functions
					function isNonTextPartiallySelected(node, range) {
						return (node.nodeType != 3)
								&& (dom.isAncestorOf(node,
										range.startContainer, true) || dom
										.isAncestorOf(node, range.endContainer,
												true));
					}

					function getRangeDocument(range) {
						return dom.getDocument(range.startContainer);
					}

					function dispatchEvent(range, type, args) {
						var listeners = range._listeners[type];
						if (listeners) {
							for ( var i = 0, len = listeners.length; i < len; ++i) {
								listeners[i].call(range, {
									target : range,
									args : args
								});
							}
						}
					}

					function getBoundaryBeforeNode(node) {
						return new DomPosition(node.parentNode, dom
								.getNodeIndex(node));
					}

					function getBoundaryAfterNode(node) {
						return new DomPosition(node.parentNode, dom
								.getNodeIndex(node) + 1);
					}

					function insertNodeAtPosition(node, n, o) {
						var firstNodeInserted = node.nodeType == 11 ? node.firstChild
								: node;
						if (dom.isCharacterDataNode(n)) {
							if (o == n.length) {
								dom.insertAfter(node, n);
							} else {
								n.parentNode.insertBefore(node, o == 0 ? n
										: dom.splitDataNode(n, o));
							}
						} else if (o >= n.childNodes.length) {
							n.appendChild(node);
						} else {
							n.insertBefore(node, n.childNodes[o]);
						}
						return firstNodeInserted;
					}

					function cloneSubtree(iterator) {
						var partiallySelected;
						for ( var node, frag = getRangeDocument(iterator.range)
								.createDocumentFragment(), subIterator; node = iterator
								.next();) {
							partiallySelected = iterator
									.isPartiallySelectedSubtree();

							node = node.cloneNode(!partiallySelected);
							if (partiallySelected) {
								subIterator = iterator.getSubtreeIterator();
								node.appendChild(cloneSubtree(subIterator));
								subIterator.detach(true);
							}

							if (node.nodeType == 10) { // DocumentType
								throw new DOMException("HIERARCHY_REQUEST_ERR");
							}
							frag.appendChild(node);
						}
						return frag;
					}

					function iterateSubtree(rangeIterator, func, iteratorState) {
						var it, n;
						iteratorState = iteratorState || {
							stop : false
						};
						for ( var node, subRangeIterator; node = rangeIterator
								.next();) {
							// log.debug("iterateSubtree, partially selected: "
							// + rangeIterator.isPartiallySelectedSubtree(),
							// nodeToString(node));
							if (rangeIterator.isPartiallySelectedSubtree()) {
								// The node is partially selected by the Range,
								// so we can use a new RangeIterator on the
								// portion of the
								// node selected by the Range.
								if (func(node) === false) {
									iteratorState.stop = true;
									return;
								} else {
									subRangeIterator = rangeIterator
											.getSubtreeIterator();
									iterateSubtree(subRangeIterator, func,
											iteratorState);
									subRangeIterator.detach(true);
									if (iteratorState.stop) {
										return;
									}
								}
							} else {
								// The whole node is selected, so we can use
								// efficient DOM iteration to iterate over the
								// node and its
								// descendant
								it = dom.createIterator(node);
								while ((n = it.next())) {
									if (func(n) === false) {
										iteratorState.stop = true;
										return;
									}
								}
							}
						}
					}

					function deleteSubtree(iterator) {
						var subIterator;
						while (iterator.next()) {
							if (iterator.isPartiallySelectedSubtree()) {
								subIterator = iterator.getSubtreeIterator();
								deleteSubtree(subIterator);
								subIterator.detach(true);
							} else {
								iterator.remove();
							}
						}
					}

					function extractSubtree(iterator) {

						for ( var node, frag = getRangeDocument(iterator.range)
								.createDocumentFragment(), subIterator; node = iterator
								.next();) {

							if (iterator.isPartiallySelectedSubtree()) {
								node = node.cloneNode(false);
								subIterator = iterator.getSubtreeIterator();
								node.appendChild(extractSubtree(subIterator));
								subIterator.detach(true);
							} else {
								iterator.remove();
							}
							if (node.nodeType == 10) { // DocumentType
								throw new DOMException("HIERARCHY_REQUEST_ERR");
							}
							frag.appendChild(node);
						}
						return frag;
					}

					function getNodesInRange(range, nodeTypes, filter) {
						// log.info("getNodesInRange, " + nodeTypes.join(","));
						var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
						var filterExists = !!filter;
						if (filterNodeTypes) {
							regex = new RegExp("^(" + nodeTypes.join("|")
									+ ")$");
						}

						var nodes = [];
						iterateSubtree(
								new RangeIterator(range, false),
								function(node) {
									if ((!filterNodeTypes || regex
											.test(node.nodeType))
											&& (!filterExists || filter(node))) {
										nodes.push(node);
									}
								});
						return nodes;
					}

					function inspect(range) {
						var name = (typeof range.getName == "undefined") ? "Range"
								: range.getName();
						return "[" + name + "("
								+ dom.inspectNode(range.startContainer) + ":"
								+ range.startOffset + ", "
								+ dom.inspectNode(range.endContainer) + ":"
								+ range.endOffset + ")]";
					}

					/*----------------------------------------------------------------------------------------------------------------*/

					// RangeIterator code partially borrows from IERange by Tim
					// Ryan (http://github.com/timcameronryan/IERange)
					/**
					 * @constructor
					 */
					function RangeIterator(range,
							clonePartiallySelectedTextNodes) {
						this.range = range;
						this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;

						if (!range.collapsed) {
							this.sc = range.startContainer;
							this.so = range.startOffset;
							this.ec = range.endContainer;
							this.eo = range.endOffset;
							var root = range.commonAncestorContainer;

							if (this.sc === this.ec
									&& dom.isCharacterDataNode(this.sc)) {
								this.isSingleCharacterDataNode = true;
								this._first = this._last = this._next = this.sc;
							} else {
								this._first = this._next = (this.sc === root && !dom
										.isCharacterDataNode(this.sc)) ? this.sc.childNodes[this.so]
										: dom.getClosestAncestorIn(this.sc,
												root, true);
								this._last = (this.ec === root && !dom
										.isCharacterDataNode(this.ec)) ? this.ec.childNodes[this.eo - 1]
										: dom.getClosestAncestorIn(this.ec,
												root, true);
							}

						}
					}

					RangeIterator.prototype = {
						_current : null,
						_next : null,
						_first : null,
						_last : null,
						isSingleCharacterDataNode : false,

						reset : function() {
							this._current = null;
							this._next = this._first;
						},

						hasNext : function() {
							return !!this._next;
						},

						next : function() {
							// Move to next node
							var current = this._current = this._next;
							if (current) {
								this._next = (current !== this._last) ? current.nextSibling
										: null;

								// Check for partially selected text nodes
								if (dom.isCharacterDataNode(current)
										&& this.clonePartiallySelectedTextNodes) {
									if (current === this.ec) {

										(current = current.cloneNode(true))
												.deleteData(this.eo,
														current.length
																- this.eo);
									}
									if (this._current === this.sc) {

										(current = current.cloneNode(true))
												.deleteData(0, this.so);
									}
								}
							}

							return current;
						},

						remove : function() {
							var current = this._current, start, end;

							if (dom.isCharacterDataNode(current)
									&& (current === this.sc || current === this.ec)) {
								start = (current === this.sc) ? this.so : 0;
								end = (current === this.ec) ? this.eo
										: current.length;
								if (start != end) {
									current.deleteData(start, end - start);
								}
							} else {
								if (current.parentNode) {
									current.parentNode.removeChild(current);
								} else {

								}
							}
						},

						// Checks if the current node is partially selected
						isPartiallySelectedSubtree : function() {
							var current = this._current;
							return isNonTextPartiallySelected(current,
									this.range);
						},

						getSubtreeIterator : function() {
							var subRange;
							if (this.isSingleCharacterDataNode) {
								subRange = this.range.cloneRange();
								subRange.collapse();
							} else {
								subRange = new Range(
										getRangeDocument(this.range));
								var current = this._current;
								var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom
										.getNodeLength(current);

								if (dom.isAncestorOf(current, this.sc, true)) {
									startContainer = this.sc;
									startOffset = this.so;
								}
								if (dom.isAncestorOf(current, this.ec, true)) {
									endContainer = this.ec;
									endOffset = this.eo;
								}

								updateBoundaries(subRange, startContainer,
										startOffset, endContainer, endOffset);
							}
							return new RangeIterator(subRange,
									this.clonePartiallySelectedTextNodes);
						},

						detach : function(detachRange) {
							if (detachRange) {
								this.range.detach();
							}
							this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
						}
					};

					/*----------------------------------------------------------------------------------------------------------------*/

					// Exceptions
					/**
					 * @constructor
					 */
					function RangeException(codeName) {
						this.code = this[codeName];
						this.codeName = codeName;
						this.message = "RangeException: " + this.codeName;
					}

					RangeException.prototype = {
						BAD_BOUNDARYPOINTS_ERR : 1,
						INVALID_NODE_TYPE_ERR : 2
					};

					RangeException.prototype.toString = function() {
						return this.message;
					};

					/*----------------------------------------------------------------------------------------------------------------*/

					/**
					 * Currently iterates through all nodes in the range on
					 * creation until I think of a decent way to do it TODO:
					 * Look into making this a proper iterator, not requiring
					 * preloading everything first
					 * 
					 * @constructor
					 */
					function RangeNodeIterator(range, nodeTypes, filter) {
						this.nodes = getNodesInRange(range, nodeTypes, filter);
						this._next = this.nodes[0];
						this._position = 0;
					}

					RangeNodeIterator.prototype = {
						_current : null,

						hasNext : function() {
							return !!this._next;
						},

						next : function() {
							this._current = this._next;
							this._next = this.nodes[++this._position];
							return this._current;
						},

						detach : function() {
							this._current = this._next = this.nodes = null;
						}
					};

					var beforeAfterNodeTypes = [ 1, 3, 4, 5, 7, 8, 10 ];
					var rootContainerNodeTypes = [ 2, 9, 11 ];
					var readonlyNodeTypes = [ 5, 6, 10, 12 ];
					var insertableNodeTypes = [ 1, 3, 4, 5, 7, 8, 10, 11 ];
					var surroundNodeTypes = [ 1, 3, 4, 5, 7, 8 ];

					function createAncestorFinder(nodeTypes) {
						return function(node, selfIsAncestor) {
							var t, n = selfIsAncestor ? node : node.parentNode;
							while (n) {
								t = n.nodeType;
								if (dom.arrayContains(nodeTypes, t)) {
									return n;
								}
								n = n.parentNode;
							}
							return null;
						};
					}

					var getRootContainer = dom.getRootContainer;
					var getDocumentOrFragmentContainer = createAncestorFinder([
							9, 11 ]);
					var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
					var getDocTypeNotationEntityAncestor = createAncestorFinder([
							6, 10, 12 ]);

					function assertNoDocTypeNotationEntityAncestor(node,
							allowSelf) {
						if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
							throw new RangeException("INVALID_NODE_TYPE_ERR");
						}
					}

					function assertNotDetached(range) {
						if (!range.startContainer) {
							throw new DOMException("INVALID_STATE_ERR");
						}
					}

					function assertValidNodeType(node, invalidTypes) {
						if (!dom.arrayContains(invalidTypes, node.nodeType)) {
							throw new RangeException("INVALID_NODE_TYPE_ERR");
						}
					}

					function assertValidOffset(node, offset) {
						if (offset < 0
								|| offset > (dom.isCharacterDataNode(node) ? node.length
										: node.childNodes.length)) {
							throw new DOMException("INDEX_SIZE_ERR");
						}
					}

					function assertSameDocumentOrFragment(node1, node2) {
						if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(
								node2, true)) {
							throw new DOMException("WRONG_DOCUMENT_ERR");
						}
					}

					function assertNodeNotReadOnly(node) {
						if (getReadonlyAncestor(node, true)) {
							throw new DOMException(
									"NO_MODIFICATION_ALLOWED_ERR");
						}
					}

					function assertNode(node, codeName) {
						if (!node) {
							throw new DOMException(codeName);
						}
					}

					function isOrphan(node) {
						return !dom.arrayContains(rootContainerNodeTypes,
								node.nodeType)
								&& !getDocumentOrFragmentContainer(node, true);
					}

					function isValidOffset(node, offset) {
						return offset <= (dom.isCharacterDataNode(node) ? node.length
								: node.childNodes.length);
					}

					function assertRangeValid(range) {
						assertNotDetached(range);
						if (isOrphan(range.startContainer)
								|| isOrphan(range.endContainer)
								|| !isValidOffset(range.startContainer,
										range.startOffset)
								|| !isValidOffset(range.endContainer,
										range.endOffset)) {
							throw new Error(
									"Range error: Range is no longer valid after DOM mutation ("
											+ range.inspect() + ")");
						}
					}

					/*----------------------------------------------------------------------------------------------------------------*/

					// Test the browser's innerHTML support to decide how to
					// implement createContextualFragment
					var styleEl = document.createElement("style");
					var htmlParsingConforms = false;
					try {
						styleEl.innerHTML = "<b>x</b>";
						htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera
						// incorrectly
						// creates
						// an
						// element
						// node
					} catch (e) {
						// IE 6 and 7 throw
					}

					api.features.htmlParsingConforms = htmlParsingConforms;

					var createContextualFragment = htmlParsingConforms ?

					// Implementation as per HTML parsing spec, trusting in the
					// browser's implementation of innerHTML. See
					// discussion and base code for this implementation at issue
					// 67.
					// Spec:
					// http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
					// Thanks to Aleks Williams.
					function(fragmentStr) {
						// "Let node the context object's start's node."
						var node = this.startContainer;
						var doc = dom.getDocument(node);

						// "If the context object's start's node is null, raise
						// an INVALID_STATE_ERR
						// exception and abort these steps."
						if (!node) {
							throw new DOMException("INVALID_STATE_ERR");
						}

						// "Let element be as follows, depending on node's
						// interface:"
						// Document, Document Fragment: null
						var el = null;

						// "Element: node"
						if (node.nodeType == 1) {
							el = node;

							// "Text, Comment: node's parentElement"
						} else if (dom.isCharacterDataNode(node)) {
							el = dom.parentElement(node);
						}

						// "If either element is null or element's ownerDocument
						// is an HTML document
						// and element's local name is "html" and element's
						// namespace is the HTML
						// namespace"
						if (el === null
								|| (el.nodeName == "HTML"
										&& dom
												.isHtmlNamespace(dom
														.getDocument(el).documentElement) && dom
										.isHtmlNamespace(el))) {

							// "let element be a new Element with "body" as its
							// local name and the HTML
							// namespace as its namespace.""
							el = doc.createElement("body");
						} else {
							el = el.cloneNode(false);
						}

						// "If the node's document is an HTML document: Invoke
						// the HTML fragment parsing algorithm."
						// "If the node's document is an XML document: Invoke
						// the XML fragment parsing algorithm."
						// "In either case, the algorithm must be invoked with
						// fragment as the input
						// and element as the context element."
						el.innerHTML = fragmentStr;

						// "If this raises an exception, then abort these steps.
						// Otherwise, let new
						// children be the nodes returned."

						// "Let fragment be a new DocumentFragment."
						// "Append all new children to fragment."
						// "Return fragment."
						return dom.fragmentFromNodeChildren(el);
					}
							:

							// In this case, innerHTML cannot be trusted, so
							// fall back to a simpler, non-conformant
							// implementation that
							// previous versions of Rangy used (with the
							// exception of using a body element rather than a
							// div)
							function(fragmentStr) {
								assertNotDetached(this);
								var doc = getRangeDocument(this);
								var el = doc.createElement("body");
								el.innerHTML = fragmentStr;

								return dom.fragmentFromNodeChildren(el);
							};

					/*----------------------------------------------------------------------------------------------------------------*/

					var rangeProperties = [ "startContainer", "startOffset",
							"endContainer", "endOffset", "collapsed",
							"commonAncestorContainer" ];

					var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
					var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;

					function RangePrototype() {
					}

					RangePrototype.prototype = {
						attachListener : function(type, listener) {
							this._listeners[type].push(listener);
						},

						compareBoundaryPoints : function(how, range) {
							assertRangeValid(this);
							assertSameDocumentOrFragment(this.startContainer,
									range.startContainer);

							var nodeA, offsetA, nodeB, offsetB;
							var prefixA = (how == e2s || how == s2s) ? "start"
									: "end";
							var prefixB = (how == s2e || how == s2s) ? "start"
									: "end";
							nodeA = this[prefixA + "Container"];
							offsetA = this[prefixA + "Offset"];
							nodeB = range[prefixB + "Container"];
							offsetB = range[prefixB + "Offset"];
							return dom.comparePoints(nodeA, offsetA, nodeB,
									offsetB);
						},

						insertNode : function(node) {
							assertRangeValid(this);
							assertValidNodeType(node, insertableNodeTypes);
							assertNodeNotReadOnly(this.startContainer);

							if (dom.isAncestorOf(node, this.startContainer,
									true)) {
								throw new DOMException("HIERARCHY_REQUEST_ERR");
							}

							// No check for whether the container of the start
							// of the Range is of a type that does not allow
							// children of the type of node: the browser's DOM
							// implementation should do this for us when we
							// attempt
							// to add the node

							var firstNodeInserted = insertNodeAtPosition(node,
									this.startContainer, this.startOffset);
							this.setStartBefore(firstNodeInserted);
						},

						cloneContents : function() {
							assertRangeValid(this);

							var clone, frag;
							if (this.collapsed) {
								return getRangeDocument(this)
										.createDocumentFragment();
							} else {
								if (this.startContainer === this.endContainer
										&& dom
												.isCharacterDataNode(this.startContainer)) {
									clone = this.startContainer.cloneNode(true);
									clone.data = clone.data.slice(
											this.startOffset, this.endOffset);
									frag = getRangeDocument(this)
											.createDocumentFragment();
									frag.appendChild(clone);
									return frag;
								} else {
									var iterator = new RangeIterator(this, true);
									clone = cloneSubtree(iterator);
									iterator.detach();
								}
								return clone;
							}
						},

						canSurroundContents : function() {
							assertRangeValid(this);
							assertNodeNotReadOnly(this.startContainer);
							assertNodeNotReadOnly(this.endContainer);

							// Check if the contents can be surrounded.
							// Specifically, this means whether the range
							// partially selects
							// no non-text nodes.
							var iterator = new RangeIterator(this, true);
							var boundariesInvalid = (iterator._first
									&& (isNonTextPartiallySelected(
											iterator._first, this)) || (iterator._last && isNonTextPartiallySelected(
									iterator._last, this)));
							iterator.detach();
							return !boundariesInvalid;
						},

						surroundContents : function(node) {
							assertValidNodeType(node, surroundNodeTypes);

							if (!this.canSurroundContents()) {
								throw new RangeException(
										"BAD_BOUNDARYPOINTS_ERR");
							}

							// Extract the contents
							var content = this.extractContents();

							// Clear the children of the node
							if (node.hasChildNodes()) {
								while (node.lastChild) {
									node.removeChild(node.lastChild);
								}
							}

							// Insert the new node and add the extracted
							// contents
							insertNodeAtPosition(node, this.startContainer,
									this.startOffset);
							node.appendChild(content);

							this.selectNode(node);
						},

						cloneRange : function() {
							assertRangeValid(this);
							var range = new Range(getRangeDocument(this));
							var i = rangeProperties.length, prop;
							while (i--) {
								prop = rangeProperties[i];
								range[prop] = this[prop];
							}
							return range;
						},

						toString : function() {
							assertRangeValid(this);
							var sc = this.startContainer;
							if (sc === this.endContainer
									&& dom.isCharacterDataNode(sc)) {
								return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data
										.slice(this.startOffset, this.endOffset)
										: "";
							} else {
								var textBits = [], iterator = new RangeIterator(
										this, true);

								iterateSubtree(iterator, function(node) {
									// Accept only text or CDATA nodes, not
									// comments

									if (node.nodeType == 3
											|| node.nodeType == 4) {
										textBits.push(node.data);
									}
								});
								iterator.detach();
								return textBits.join("");
							}
						},

						// The methods below are all non-standard. The following
						// batch were introduced by Mozilla but have since
						// been removed from Mozilla.

						compareNode : function(node) {
							assertRangeValid(this);

							var parent = node.parentNode;
							var nodeIndex = dom.getNodeIndex(node);

							if (!parent) {
								throw new DOMException("NOT_FOUND_ERR");
							}

							var startComparison = this.comparePoint(parent,
									nodeIndex), endComparison = this
									.comparePoint(parent, nodeIndex + 1);

							if (startComparison < 0) { // Node starts before
								return (endComparison > 0) ? n_b_a : n_b;
							} else {
								return (endComparison > 0) ? n_a : n_i;
							}
						},

						comparePoint : function(node, offset) {
							assertRangeValid(this);
							assertNode(node, "HIERARCHY_REQUEST_ERR");
							assertSameDocumentOrFragment(node,
									this.startContainer);

							if (dom.comparePoints(node, offset,
									this.startContainer, this.startOffset) < 0) {
								return -1;
							} else if (dom.comparePoints(node, offset,
									this.endContainer, this.endOffset) > 0) {
								return 1;
							}
							return 0;
						},

						createContextualFragment : createContextualFragment,

						toHtml : function() {
							assertRangeValid(this);
							var container = getRangeDocument(this)
									.createElement("div");
							container.appendChild(this.cloneContents());
							return container.innerHTML;
						},

						// touchingIsIntersecting determines whether this method
						// considers a node that borders a range intersects
						// with it (as in WebKit) or not (as in Gecko pre-1.9,
						// and the default)
						intersectsNode : function(node, touchingIsIntersecting) {
							assertRangeValid(this);
							assertNode(node, "NOT_FOUND_ERR");
							if (dom.getDocument(node) !== getRangeDocument(this)) {
								return false;
							}

							var parent = node.parentNode, offset = dom
									.getNodeIndex(node);
							assertNode(parent, "NOT_FOUND_ERR");

							var startComparison = dom.comparePoints(parent,
									offset, this.endContainer, this.endOffset), endComparison = dom
									.comparePoints(parent, offset + 1,
											this.startContainer,
											this.startOffset);

							return touchingIsIntersecting ? startComparison <= 0
									&& endComparison >= 0
									: startComparison < 0 && endComparison > 0;
						},

						isPointInRange : function(node, offset) {
							assertRangeValid(this);
							assertNode(node, "HIERARCHY_REQUEST_ERR");
							assertSameDocumentOrFragment(node,
									this.startContainer);

							return (dom.comparePoints(node, offset,
									this.startContainer, this.startOffset) >= 0)
									&& (dom.comparePoints(node, offset,
											this.endContainer, this.endOffset) <= 0);
						},

						// The methods below are non-standard and invented by
						// me.

						// Sharing a boundary start-to-end or end-to-start does
						// not count as intersection.
						intersectsRange : function(range,
								touchingIsIntersecting) {
							assertRangeValid(this);

							if (getRangeDocument(range) != getRangeDocument(this)) {
								throw new DOMException("WRONG_DOCUMENT_ERR");
							}

							var startComparison = dom.comparePoints(
									this.startContainer, this.startOffset,
									range.endContainer, range.endOffset), endComparison = dom
									.comparePoints(this.endContainer,
											this.endOffset,
											range.startContainer,
											range.startOffset);

							return touchingIsIntersecting ? startComparison <= 0
									&& endComparison >= 0
									: startComparison < 0 && endComparison > 0;
						},

						intersection : function(range) {
							if (this.intersectsRange(range)) {
								var startComparison = dom
										.comparePoints(this.startContainer,
												this.startOffset,
												range.startContainer,
												range.startOffset), endComparison = dom
										.comparePoints(this.endContainer,
												this.endOffset,
												range.endContainer,
												range.endOffset);

								var intersectionRange = this.cloneRange();

								if (startComparison == -1) {
									intersectionRange.setStart(
											range.startContainer,
											range.startOffset);
								}
								if (endComparison == 1) {
									intersectionRange
											.setEnd(range.endContainer,
													range.endOffset);
								}
								return intersectionRange;
							}
							return null;
						},

						union : function(range) {
							if (this.intersectsRange(range, true)) {
								var unionRange = this.cloneRange();
								if (dom.comparePoints(range.startContainer,
										range.startOffset, this.startContainer,
										this.startOffset) == -1) {
									unionRange.setStart(range.startContainer,
											range.startOffset);
								}
								if (dom.comparePoints(range.endContainer,
										range.endOffset, this.endContainer,
										this.endOffset) == 1) {
									unionRange.setEnd(range.endContainer,
											range.endOffset);
								}
								return unionRange;
							} else {
								throw new RangeException(
										"Ranges do not intersect");
							}
						},

						containsNode : function(node, allowPartial) {
							if (allowPartial) {
								return this.intersectsNode(node, false);
							} else {
								return this.compareNode(node) == n_i;
							}
						},

						containsNodeContents : function(node) {
							return this.comparePoint(node, 0) >= 0
									&& this.comparePoint(node, dom
											.getNodeLength(node)) <= 0;
						},

						containsRange : function(range) {
							return this.intersection(range).equals(range);
						},

						containsNodeText : function(node) {
							var nodeRange = this.cloneRange();
							nodeRange.selectNode(node);
							var textNodes = nodeRange.getNodes([ 3 ]);
							if (textNodes.length > 0) {
								nodeRange.setStart(textNodes[0], 0);
								var lastTextNode = textNodes.pop();
								nodeRange.setEnd(lastTextNode,
										lastTextNode.length);
								var contains = this.containsRange(nodeRange);
								nodeRange.detach();
								return contains;
							} else {
								return this.containsNodeContents(node);
							}
						},

						createNodeIterator : function(nodeTypes, filter) {
							assertRangeValid(this);
							return new RangeNodeIterator(this, nodeTypes,
									filter);
						},

						getNodes : function(nodeTypes, filter) {
							assertRangeValid(this);
							return getNodesInRange(this, nodeTypes, filter);
						},

						getDocument : function() {
							return getRangeDocument(this);
						},

						collapseBefore : function(node) {
							assertNotDetached(this);

							this.setEndBefore(node);
							this.collapse(false);
						},

						collapseAfter : function(node) {
							assertNotDetached(this);

							this.setStartAfter(node);
							this.collapse(true);
						},

						getName : function() {
							return "DomRange";
						},

						equals : function(range) {
							return Range.rangesEqual(this, range);
						},

						inspect : function() {
							return inspect(this);
						}
					};

					function copyComparisonConstantsToObject(obj) {
						obj.START_TO_START = s2s;
						obj.START_TO_END = s2e;
						obj.END_TO_END = e2e;
						obj.END_TO_START = e2s;

						obj.NODE_BEFORE = n_b;
						obj.NODE_AFTER = n_a;
						obj.NODE_BEFORE_AND_AFTER = n_b_a;
						obj.NODE_INSIDE = n_i;
					}

					function copyComparisonConstants(constructor) {
						copyComparisonConstantsToObject(constructor);
						copyComparisonConstantsToObject(constructor.prototype);
					}

					function createRangeContentRemover(remover, boundaryUpdater) {
						return function() {
							assertRangeValid(this);

							var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;

							var iterator = new RangeIterator(this, true);

							// Work out where to position the range after
							// content removal
							var node, boundary;
							if (sc !== root) {
								node = dom.getClosestAncestorIn(sc, root, true);
								boundary = getBoundaryAfterNode(node);
								sc = boundary.node;
								so = boundary.offset;
							}

							// Check none of the range is read-only
							iterateSubtree(iterator, assertNodeNotReadOnly);

							iterator.reset();

							// Remove the content
							var returnValue = remover(iterator);
							iterator.detach();

							// Move to the new position
							boundaryUpdater(this, sc, so, sc, so);

							return returnValue;
						};
					}

					function createPrototypeRange(constructor, boundaryUpdater,
							detacher) {
						function createBeforeAfterNodeSetter(isBefore, isStart) {
							return function(node) {
								assertNotDetached(this);
								assertValidNodeType(node, beforeAfterNodeTypes);
								assertValidNodeType(getRootContainer(node),
										rootContainerNodeTypes);

								var boundary = (isBefore ? getBoundaryBeforeNode
										: getBoundaryAfterNode)(node);
								(isStart ? setRangeStart : setRangeEnd)(this,
										boundary.node, boundary.offset);
							};
						}

						function setRangeStart(range, node, offset) {
							var ec = range.endContainer, eo = range.endOffset;
							if (node !== range.startContainer
									|| offset !== range.startOffset) {
								// Check the root containers of the range and
								// the new boundary, and also check whether the
								// new boundary
								// is after the current end. In either case,
								// collapse the range to the new position
								if (getRootContainer(node) != getRootContainer(ec)
										|| dom.comparePoints(node, offset, ec,
												eo) == 1) {
									ec = node;
									eo = offset;
								}
								boundaryUpdater(range, node, offset, ec, eo);
							}
						}

						function setRangeEnd(range, node, offset) {
							var sc = range.startContainer, so = range.startOffset;
							if (node !== range.endContainer
									|| offset !== range.endOffset) {
								// Check the root containers of the range and
								// the new boundary, and also check whether the
								// new boundary
								// is after the current end. In either case,
								// collapse the range to the new position
								if (getRootContainer(node) != getRootContainer(sc)
										|| dom.comparePoints(node, offset, sc,
												so) == -1) {
									sc = node;
									so = offset;
								}
								boundaryUpdater(range, sc, so, node, offset);
							}
						}

						function setRangeStartAndEnd(range, node, offset) {
							if (node !== range.startContainer
									|| offset !== range.startOffset
									|| node !== range.endContainer
									|| offset !== range.endOffset) {
								boundaryUpdater(range, node, offset, node,
										offset);
							}
						}

						constructor.prototype = new RangePrototype();

						api.util
								.extend(
										constructor.prototype,
										{
											setStart : function(node, offset) {
												assertNotDetached(this);
												assertNoDocTypeNotationEntityAncestor(
														node, true);
												assertValidOffset(node, offset);

												setRangeStart(this, node,
														offset);
											},

											setEnd : function(node, offset) {
												assertNotDetached(this);
												assertNoDocTypeNotationEntityAncestor(
														node, true);
												assertValidOffset(node, offset);

												setRangeEnd(this, node, offset);
											},

											setStartBefore : createBeforeAfterNodeSetter(
													true, true),
											setStartAfter : createBeforeAfterNodeSetter(
													false, true),
											setEndBefore : createBeforeAfterNodeSetter(
													true, false),
											setEndAfter : createBeforeAfterNodeSetter(
													false, false),

											collapse : function(isStart) {
												assertRangeValid(this);
												if (isStart) {
													boundaryUpdater(
															this,
															this.startContainer,
															this.startOffset,
															this.startContainer,
															this.startOffset);
												} else {
													boundaryUpdater(this,
															this.endContainer,
															this.endOffset,
															this.endContainer,
															this.endOffset);
												}
											},

											selectNodeContents : function(node) {
												// This doesn't seem well
												// specified: the spec talks
												// only about selecting the
												// node's contents, which
												// could be taken to mean only
												// its children. However,
												// browsers implement this the
												// same as selectNode for
												// text nodes, so I shall do
												// likewise
												assertNotDetached(this);
												assertNoDocTypeNotationEntityAncestor(
														node, true);

												boundaryUpdater(this, node, 0,
														node,
														dom.getNodeLength(node));
											},

											selectNode : function(node) {
												assertNotDetached(this);
												assertNoDocTypeNotationEntityAncestor(
														node, false);
												assertValidNodeType(node,
														beforeAfterNodeTypes);

												var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
												boundaryUpdater(this,
														start.node,
														start.offset, end.node,
														end.offset);
											},

											extractContents : createRangeContentRemover(
													extractSubtree,
													boundaryUpdater),

											deleteContents : createRangeContentRemover(
													deleteSubtree,
													boundaryUpdater),

											canSurroundContents : function() {
												assertRangeValid(this);
												assertNodeNotReadOnly(this.startContainer);
												assertNodeNotReadOnly(this.endContainer);

												// Check if the contents can be
												// surrounded. Specifically,
												// this means whether the range
												// partially selects
												// no non-text nodes.
												var iterator = new RangeIterator(
														this, true);
												var boundariesInvalid = (iterator._first
														&& (isNonTextPartiallySelected(
																iterator._first,
																this)) || (iterator._last && isNonTextPartiallySelected(
														iterator._last, this)));
												iterator.detach();
												return !boundariesInvalid;
											},

											detach : function() {
												detacher(this);
											},

											splitBoundaries : function() {
												assertRangeValid(this);

												var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
												var startEndSame = (sc === ec);

												if (dom.isCharacterDataNode(ec)
														&& eo > 0
														&& eo < ec.length) {
													dom.splitDataNode(ec, eo);

												}

												if (dom.isCharacterDataNode(sc)
														&& so > 0
														&& so < sc.length) {

													sc = dom.splitDataNode(sc,
															so);
													if (startEndSame) {
														eo -= so;
														ec = sc;
													} else if (ec == sc.parentNode
															&& eo >= dom
																	.getNodeIndex(sc)) {
														eo++;
													}
													so = 0;

												}
												boundaryUpdater(this, sc, so,
														ec, eo);
											},

											normalizeBoundaries : function() {
												assertRangeValid(this);

												var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;

												var mergeForward = function(
														node) {
													var sibling = node.nextSibling;
													if (sibling
															&& sibling.nodeType == node.nodeType) {
														ec = node;
														eo = node.length;
														node
																.appendData(sibling.data);
														sibling.parentNode
																.removeChild(sibling);
													}
												};

												var mergeBackward = function(
														node) {
													var sibling = node.previousSibling;
													if (sibling
															&& sibling.nodeType == node.nodeType) {
														sc = node;
														var nodeLength = node.length;
														so = sibling.length;
														node.insertData(0,
																sibling.data);
														sibling.parentNode
																.removeChild(sibling);
														if (sc == ec) {
															eo += so;
															ec = sc;
														} else if (ec == node.parentNode) {
															var nodeIndex = dom
																	.getNodeIndex(node);
															if (eo == nodeIndex) {
																ec = node;
																eo = nodeLength;
															} else if (eo > nodeIndex) {
																eo--;
															}
														}
													}
												};

												var normalizeStart = true;

												if (dom.isCharacterDataNode(ec)) {
													if (ec.length == eo) {
														mergeForward(ec);
													}
												} else {
													if (eo > 0) {
														var endNode = ec.childNodes[eo - 1];
														if (endNode
																&& dom
																		.isCharacterDataNode(endNode)) {
															mergeForward(endNode);
														}
													}
													normalizeStart = !this.collapsed;
												}

												if (normalizeStart) {
													if (dom
															.isCharacterDataNode(sc)) {
														if (so == 0) {
															mergeBackward(sc);
														}
													} else {
														if (so < sc.childNodes.length) {
															var startNode = sc.childNodes[so];
															if (startNode
																	&& dom
																			.isCharacterDataNode(startNode)) {
																mergeBackward(startNode);
															}
														}
													}
												} else {
													sc = ec;
													so = eo;
												}

												boundaryUpdater(this, sc, so,
														ec, eo);
											},

											collapseToPoint : function(node,
													offset) {
												assertNotDetached(this);

												assertNoDocTypeNotationEntityAncestor(
														node, true);
												assertValidOffset(node, offset);

												setRangeStartAndEnd(this, node,
														offset);
											}
										});

						copyComparisonConstants(constructor);
					}

					/*----------------------------------------------------------------------------------------------------------------*/

					// Updates commonAncestorContainer and collapsed after
					// boundary change
					function updateCollapsedAndCommonAncestor(range) {
						range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
						range.commonAncestorContainer = range.collapsed ? range.startContainer
								: dom.getCommonAncestor(range.startContainer,
										range.endContainer);
					}

					function updateBoundaries(range, startContainer,
							startOffset, endContainer, endOffset) {
						var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
						var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);

						range.startContainer = startContainer;
						range.startOffset = startOffset;
						range.endContainer = endContainer;
						range.endOffset = endOffset;

						updateCollapsedAndCommonAncestor(range);
						dispatchEvent(range, "boundarychange", {
							startMoved : startMoved,
							endMoved : endMoved
						});
					}

					function detach(range) {
						assertNotDetached(range);
						range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
						range.collapsed = range.commonAncestorContainer = null;
						dispatchEvent(range, "detach", null);
						range._listeners = null;
					}

					/**
					 * @constructor
					 */
					function Range(doc) {
						this.startContainer = doc;
						this.startOffset = 0;
						this.endContainer = doc;
						this.endOffset = 0;
						this._listeners = {
							boundarychange : [],
							detach : []
						};
						updateCollapsedAndCommonAncestor(this);
					}

					createPrototypeRange(Range, updateBoundaries, detach);

					api.rangePrototype = RangePrototype.prototype;

					Range.rangeProperties = rangeProperties;
					Range.RangeIterator = RangeIterator;
					Range.copyComparisonConstants = copyComparisonConstants;
					Range.createPrototypeRange = createPrototypeRange;
					Range.inspect = inspect;
					Range.getRangeDocument = getRangeDocument;
					Range.rangesEqual = function(r1, r2) {
						return r1.startContainer === r2.startContainer
								&& r1.startOffset === r2.startOffset
								&& r1.endContainer === r2.endContainer
								&& r1.endOffset === r2.endOffset;
					};

					api.DomRange = Range;
					api.RangeException = RangeException;
				});
rangy
		.createModule(
				"WrappedRange",
				function(api, module) {
					api.requireModules([ "DomUtil", "DomRange" ]);

					/**
					 * @constructor
					 */
					var WrappedRange;
					var dom = api.dom;
					var DomPosition = dom.DomPosition;
					var DomRange = api.DomRange;

					/*----------------------------------------------------------------------------------------------------------------*/

					/*
					 * This is a workaround for a bug where IE returns the wrong
					 * container element from the TextRange's parentElement()
					 * method. For example, in the following (where pipes denote
					 * the selection boundaries):
					 * 
					 * <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
					 * 
					 * var range = document.selection.createRange();
					 * alert(range.parentElement().id); // Should alert "ul" but
					 * alerts "b"
					 * 
					 * This method returns the common ancestor node of the
					 * following: - the parentElement() of the textRange - the
					 * parentElement() of the textRange after calling
					 * collapse(true) - the parentElement() of the textRange
					 * after calling collapse(false)
					 */
					function getTextRangeContainerElement(textRange) {
						var parentEl = textRange.parentElement();

						var range = textRange.duplicate();
						range.collapse(true);
						var startEl = range.parentElement();
						range = textRange.duplicate();
						range.collapse(false);
						var endEl = range.parentElement();
						var startEndContainer = (startEl == endEl) ? startEl
								: dom.getCommonAncestor(startEl, endEl);

						return startEndContainer == parentEl ? startEndContainer
								: dom.getCommonAncestor(parentEl,
										startEndContainer);
					}

					function textRangeIsCollapsed(textRange) {
						return textRange.compareEndPoints("StartToEnd",
								textRange) == 0;
					}

					// Gets the boundary of a TextRange expressed as a node and
					// an offset within that node. This function started out as
					// an improved version of code found in Tim Cameron Ryan's
					// IERange (http://code.google.com/p/ierange/) but has
					// grown, fixing problems with line breaks in preformatted
					// text, adding workaround for IE TextRange bugs, handling
					// for inputs and images, plus optimizations.
					function getTextRangeBoundaryPosition(textRange,
							wholeRangeContainerElement, isStart, isCollapsed) {
						var workingRange = textRange.duplicate();

						workingRange.collapse(isStart);
						var containerElement = workingRange.parentElement();

						// Sometimes collapsing a TextRange that's at the start
						// of a text node can move it into the previous node, so
						// check for that
						// TODO: Find out when. Workaround for
						// wholeRangeContainerElement may break this
						if (!dom.isAncestorOf(wholeRangeContainerElement,
								containerElement, true)) {
							containerElement = wholeRangeContainerElement;

						}

						// Deal with nodes that cannot "contain rich HTML
						// markup". In practice, this means form inputs, images
						// and
						// similar. See
						// http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
						if (!containerElement.canHaveHTML) {
							return new DomPosition(containerElement.parentNode,
									dom.getNodeIndex(containerElement));
						}

						var workingNode = dom.getDocument(containerElement)
								.createElement("span");
						var comparison, workingComparisonType = isStart ? "StartToStart"
								: "StartToEnd";
						var previousNode, nextNode, boundaryPosition, boundaryNode;

						// Move the working range through the container's
						// children, starting at the end and working backwards,
						// until the
						// working range reaches or goes past the boundary we're
						// interested in
						do {
							containerElement.insertBefore(workingNode,
									workingNode.previousSibling);
							workingRange.moveToElementText(workingNode);
						} while ((comparison = workingRange.compareEndPoints(
								workingComparisonType, textRange)) > 0
								&& workingNode.previousSibling);

						// We've now reached or gone past the boundary of the
						// text range we're interested in
						// so have identified the node we want
						boundaryNode = workingNode.nextSibling;

						if (comparison == -1 && boundaryNode
								&& dom.isCharacterDataNode(boundaryNode)) {
							// This is a character data node (text, comment,
							// cdata). The working range is collapsed at the
							// start of the
							// node containing the text range's boundary, so we
							// move the end of the working range to the boundary
							// point
							// and measure the length of its text to get the
							// boundary's offset within the node.
							workingRange.setEndPoint(isStart ? "EndToStart"
									: "EndToEnd", textRange);

							var offset;

							if (/[\r\n]/.test(boundaryNode.data)) {
								/*
								 * For the particular case of a boundary within
								 * a text node containing line breaks (within a
								 * <pre> element, for example), we need a
								 * slightly complicated approach to get the
								 * boundary's offset in IE. The facts: - Each
								 * line break is represented as \r in the text
								 * node's data/nodeValue properties - Each line
								 * break is represented as \r\n in the
								 * TextRange's 'text' property - The 'text'
								 * property of the TextRange does not contain
								 * trailing line breaks
								 * 
								 * To get round the problem presented by the
								 * final fact above, we can use the fact that
								 * TextRange's moveStart() and moveEnd() methods
								 * return the actual number of characters moved,
								 * which is not necessarily the same as the
								 * number of characters it was instructed to
								 * move. The simplest approach is to use this to
								 * store the characters moved when moving both
								 * the start and end of the range to the start
								 * of the document body and subtracting the
								 * start offset from the end offset (the
								 * "move-negative-gazillion" method). However,
								 * this is extremely slow when the document is
								 * large and the range is near the end of it.
								 * Clearly doing the mirror image (i.e. moving
								 * the range boundaries to the end of the
								 * document) has the same problem.
								 * 
								 * Another approach that works is to use
								 * moveStart() to move the start boundary of the
								 * range up to the end boundary one character at
								 * a time and incrementing a counter with the
								 * value returned by the moveStart() call.
								 * However, the check for whether the start
								 * boundary has reached the end boundary is
								 * expensive, so this method is slow (although
								 * unlike "move-negative-gazillion" is largely
								 * unaffected by the location of the range
								 * within the document).
								 * 
								 * The method below is a hybrid of the two
								 * methods above. It uses the fact that a string
								 * containing the TextRange's 'text' property
								 * with each \r\n converted to a single \r
								 * character cannot be longer than the text of
								 * the TextRange, so the start of the range is
								 * moved that length initially and then a
								 * character at a time to make up for any
								 * trailing line breaks not contained in the
								 * 'text' property. This has good performance in
								 * most situations compared to the previous two
								 * methods.
								 */
								var tempRange = workingRange.duplicate();
								var rangeLength = tempRange.text.replace(
										/\r\n/g, "\r").length;

								offset = tempRange.moveStart("character",
										rangeLength);
								while ((comparison = tempRange
										.compareEndPoints("StartToEnd",
												tempRange)) == -1) {
									offset++;
									tempRange.moveStart("character", 1);
								}
							} else {
								offset = workingRange.text.length;
							}
							boundaryPosition = new DomPosition(boundaryNode,
									offset);
						} else {

							// If the boundary immediately follows a character
							// data node and this is the end boundary, we should
							// favour
							// a position within that, and likewise for a start
							// boundary preceding a character data node
							previousNode = (isCollapsed || !isStart)
									&& workingNode.previousSibling;
							nextNode = (isCollapsed || isStart)
									&& workingNode.nextSibling;

							if (nextNode && dom.isCharacterDataNode(nextNode)) {
								boundaryPosition = new DomPosition(nextNode, 0);
							} else if (previousNode
									&& dom.isCharacterDataNode(previousNode)) {
								boundaryPosition = new DomPosition(
										previousNode, previousNode.length);
							} else {
								boundaryPosition = new DomPosition(
										containerElement, dom
												.getNodeIndex(workingNode));
							}
						}

						// Clean up
						workingNode.parentNode.removeChild(workingNode);

						return boundaryPosition;
					}

					// Returns a TextRange representing the boundary of a
					// TextRange expressed as a node and an offset within that
					// node.
					// This function started out as an optimized version of code
					// found in Tim Cameron Ryan's IERange
					// (http://code.google.com/p/ierange/)
					function createBoundaryTextRange(boundaryPosition, isStart) {
						var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
						var doc = dom.getDocument(boundaryPosition.node);
						var workingNode, childNodes, workingRange = doc.body
								.createTextRange();
						var nodeIsDataNode = dom
								.isCharacterDataNode(boundaryPosition.node);

						if (nodeIsDataNode) {
							boundaryNode = boundaryPosition.node;
							boundaryParent = boundaryNode.parentNode;
						} else {
							childNodes = boundaryPosition.node.childNodes;
							boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset]
									: null;
							boundaryParent = boundaryPosition.node;
						}

						// Position the range immediately before the node
						// containing the boundary
						workingNode = doc.createElement("span");

						// Making the working element non-empty element
						// persuades IE to consider the TextRange boundary to be
						// within the
						// element rather than immediately before or after it,
						// which is what we want
						workingNode.innerHTML = "&#feff;";

						// insertBefore is supposed to work like appendChild if
						// the second parameter is null. However, a bug report
						// for IERange suggests that it can crash the browser:
						// http://code.google.com/p/ierange/issues/detail?id=12
						if (boundaryNode) {
							boundaryParent.insertBefore(workingNode,
									boundaryNode);
						} else {
							boundaryParent.appendChild(workingNode);
						}

						workingRange.moveToElementText(workingNode);
						workingRange.collapse(!isStart);

						// Clean up
						boundaryParent.removeChild(workingNode);

						// Move the working range to the text offset, if
						// required
						if (nodeIsDataNode) {
							workingRange[isStart ? "moveStart" : "moveEnd"](
									"character", boundaryOffset);
						}

						return workingRange;
					}

					/*----------------------------------------------------------------------------------------------------------------*/

					if (api.features.implementsDomRange
							&& (!api.features.implementsTextRange || !api.config.preferTextRange)) {
						// This is a wrapper around the browser's native DOM
						// Range. It has two aims:
						// - Provide workarounds for specific browser bugs
						// - provide convenient extensions, which are inherited
						// from Rangy's DomRange

						(function() {
							var rangeProto;
							var rangeProperties = DomRange.rangeProperties;
							var canSetRangeStartAfterEnd;

							function updateRangeProperties(range) {
								var i = rangeProperties.length, prop;
								while (i--) {
									prop = rangeProperties[i];
									range[prop] = range.nativeRange[prop];
								}
							}

							function updateNativeRange(range, startContainer,
									startOffset, endContainer, endOffset) {
								var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
								var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);

								// Always set both boundaries for the benefit of
								// IE9 (see issue 35)
								if (startMoved || endMoved) {
									range.setEnd(endContainer, endOffset);
									range.setStart(startContainer, startOffset);
								}
							}

							function detach(range) {
								range.nativeRange.detach();
								range.detached = true;
								var i = rangeProperties.length, prop;
								while (i--) {
									prop = rangeProperties[i];
									range[prop] = null;
								}
							}

							var createBeforeAfterNodeSetter;

							WrappedRange = function(range) {
								if (!range) {
									throw new Error("Range must be specified");
								}
								this.nativeRange = range;
								updateRangeProperties(this);
							};

							DomRange.createPrototypeRange(WrappedRange,
									updateNativeRange, detach);

							rangeProto = WrappedRange.prototype;

							rangeProto.selectNode = function(node) {
								this.nativeRange.selectNode(node);
								updateRangeProperties(this);
							};

							rangeProto.deleteContents = function() {
								this.nativeRange.deleteContents();
								updateRangeProperties(this);
							};

							rangeProto.extractContents = function() {
								var frag = this.nativeRange.extractContents();
								updateRangeProperties(this);
								return frag;
							};

							rangeProto.cloneContents = function() {
								return this.nativeRange.cloneContents();
							};

							// TODO: Until I can find a way to programmatically
							// trigger the Firefox bug (apparently
							// long-standing, still
							// present in 3.6.8) that throws "Index or size is
							// negative or greater than the allowed amount" for
							// insertNode in some circumstances, all browsers
							// will have to use the Rangy's own implementation
							// of
							// insertNode, which works but is almost certainly
							// slower than the native implementation.
							/*
							 * rangeProto.insertNode = function(node) {
							 * this.nativeRange.insertNode(node);
							 * updateRangeProperties(this); };
							 */

							rangeProto.surroundContents = function(node) {
								this.nativeRange.surroundContents(node);
								updateRangeProperties(this);
							};

							rangeProto.collapse = function(isStart) {
								this.nativeRange.collapse(isStart);
								updateRangeProperties(this);
							};

							rangeProto.cloneRange = function() {
								return new WrappedRange(this.nativeRange
										.cloneRange());
							};

							rangeProto.refresh = function() {
								updateRangeProperties(this);
							};

							rangeProto.toString = function() {
								return this.nativeRange.toString();
							};

							// Create test range and node for feature detection

							var testTextNode = document.createTextNode("test");
							dom.getBody(document).appendChild(testTextNode);
							var range = document.createRange();

							/*--------------------------------------------------------------------------------------------------------*/

							// Test for Firefox 2 bug that prevents moving the
							// start of a Range to a point after its current end
							// and
							// correct for it
							range.setStart(testTextNode, 0);
							range.setEnd(testTextNode, 0);

							try {
								range.setStart(testTextNode, 1);
								canSetRangeStartAfterEnd = true;

								rangeProto.setStart = function(node, offset) {
									this.nativeRange.setStart(node, offset);
									updateRangeProperties(this);
								};

								rangeProto.setEnd = function(node, offset) {
									this.nativeRange.setEnd(node, offset);
									updateRangeProperties(this);
								};

								createBeforeAfterNodeSetter = function(name) {
									return function(node) {
										this.nativeRange[name](node);
										updateRangeProperties(this);
									};
								};

							} catch (ex) {

								canSetRangeStartAfterEnd = false;

								rangeProto.setStart = function(node, offset) {
									try {
										this.nativeRange.setStart(node, offset);
									} catch (ex) {
										this.nativeRange.setEnd(node, offset);
										this.nativeRange.setStart(node, offset);
									}
									updateRangeProperties(this);
								};

								rangeProto.setEnd = function(node, offset) {
									try {
										this.nativeRange.setEnd(node, offset);
									} catch (ex) {
										this.nativeRange.setStart(node, offset);
										this.nativeRange.setEnd(node, offset);
									}
									updateRangeProperties(this);
								};

								createBeforeAfterNodeSetter = function(name,
										oppositeName) {
									return function(node) {
										try {
											this.nativeRange[name](node);
										} catch (ex) {
											this.nativeRange[oppositeName]
													(node);
											this.nativeRange[name](node);
										}
										updateRangeProperties(this);
									};
								};
							}

							rangeProto.setStartBefore = createBeforeAfterNodeSetter(
									"setStartBefore", "setEndBefore");
							rangeProto.setStartAfter = createBeforeAfterNodeSetter(
									"setStartAfter", "setEndAfter");
							rangeProto.setEndBefore = createBeforeAfterNodeSetter(
									"setEndBefore", "setStartBefore");
							rangeProto.setEndAfter = createBeforeAfterNodeSetter(
									"setEndAfter", "setStartAfter");

							/*--------------------------------------------------------------------------------------------------------*/

							// Test for and correct Firefox 2 behaviour with
							// selectNodeContents on text nodes: it collapses
							// the range to
							// the 0th character of the text node
							range.selectNodeContents(testTextNode);
							if (range.startContainer == testTextNode
									&& range.endContainer == testTextNode
									&& range.startOffset == 0
									&& range.endOffset == testTextNode.length) {
								rangeProto.selectNodeContents = function(node) {
									this.nativeRange.selectNodeContents(node);
									updateRangeProperties(this);
								};
							} else {
								rangeProto.selectNodeContents = function(node) {
									this.setStart(node, 0);
									this.setEnd(node, DomRange
											.getEndOffset(node));
								};
							}

							/*--------------------------------------------------------------------------------------------------------*/

							// Test for WebKit bug that has the beahviour of
							// compareBoundaryPoints round the wrong way for
							// constants
							// START_TO_END and END_TO_START:
							// https://bugs.webkit.org/show_bug.cgi?id=20738
							range.selectNodeContents(testTextNode);
							range.setEnd(testTextNode, 3);

							var range2 = document.createRange();
							range2.selectNodeContents(testTextNode);
							range2.setEnd(testTextNode, 4);
							range2.setStart(testTextNode, 2);

							if (range.compareBoundaryPoints(range.START_TO_END,
									range2) == -1
									& range.compareBoundaryPoints(
											range.END_TO_START, range2) == 1) {
								// This is the wrong way round, so correct for
								// it

								rangeProto.compareBoundaryPoints = function(
										type, range) {
									range = range.nativeRange || range;
									if (type == range.START_TO_END) {
										type = range.END_TO_START;
									} else if (type == range.END_TO_START) {
										type = range.START_TO_END;
									}
									return this.nativeRange
											.compareBoundaryPoints(type, range);
								};
							} else {
								rangeProto.compareBoundaryPoints = function(
										type, range) {
									return this.nativeRange
											.compareBoundaryPoints(type,
													range.nativeRange || range);
								};
							}

							/*--------------------------------------------------------------------------------------------------------*/

							// Test for existence of createContextualFragment
							// and delegate to it if it exists
							if (api.util.isHostMethod(range,
									"createContextualFragment")) {
								rangeProto.createContextualFragment = function(
										fragmentStr) {
									return this.nativeRange
											.createContextualFragment(fragmentStr);
								};
							}

							/*--------------------------------------------------------------------------------------------------------*/

							// Clean up
							dom.getBody(document).removeChild(testTextNode);
							range.detach();
							range2.detach();
						})();

						api.createNativeRange = function(doc) {
							doc = doc || document;
							return doc.createRange();
						};
					} else if (api.features.implementsTextRange) {
						// This is a wrapper around a TextRange, providing full
						// DOM Range functionality using rangy's DomRange as a
						// prototype

						WrappedRange = function(textRange) {
							this.textRange = textRange;
							this.refresh();
						};

						WrappedRange.prototype = new DomRange(document);

						WrappedRange.prototype.refresh = function() {
							var start, end;

							// TextRange's parentElement() method cannot be
							// trusted. getTextRangeContainerElement() works
							// around that.
							var rangeContainerElement = getTextRangeContainerElement(this.textRange);

							if (textRangeIsCollapsed(this.textRange)) {
								end = start = getTextRangeBoundaryPosition(
										this.textRange, rangeContainerElement,
										true, true);
							} else {

								start = getTextRangeBoundaryPosition(
										this.textRange, rangeContainerElement,
										true, false);
								end = getTextRangeBoundaryPosition(
										this.textRange, rangeContainerElement,
										false, false);
							}

							this.setStart(start.node, start.offset);
							this.setEnd(end.node, end.offset);
						};

						DomRange.copyComparisonConstants(WrappedRange);

						// Add WrappedRange as the Range property of the global
						// object to allow expression like Range.END_TO_END to
						// work
						var globalObj = (function() {
							return this;
						})();
						if (typeof globalObj.Range == "undefined") {
							globalObj.Range = WrappedRange;
						}

						api.createNativeRange = function(doc) {
							doc = doc || document;
							return doc.body.createTextRange();
						};
					}

					if (api.features.implementsTextRange) {
						WrappedRange.rangeToTextRange = function(range) {
							if (range.collapsed) {
								var tr = createBoundaryTextRange(
										new DomPosition(range.startContainer,
												range.startOffset), true);

								return tr;

								// return createBoundaryTextRange(new
								// DomPosition(range.startContainer,
								// range.startOffset), true);
							} else {
								var startRange = createBoundaryTextRange(
										new DomPosition(range.startContainer,
												range.startOffset), true);
								var endRange = createBoundaryTextRange(
										new DomPosition(range.endContainer,
												range.endOffset), false);
								var textRange = dom
										.getDocument(range.startContainer).body
										.createTextRange();
								textRange.setEndPoint("StartToStart",
										startRange);
								textRange.setEndPoint("EndToEnd", endRange);
								return textRange;
							}
						};
					}

					WrappedRange.prototype.getName = function() {
						return "WrappedRange";
					};

					api.WrappedRange = WrappedRange;

					api.createRange = function(doc) {
						doc = doc || document;
						return new WrappedRange(api.createNativeRange(doc));
					};

					api.createRangyRange = function(doc) {
						doc = doc || document;
						return new DomRange(doc);
					};

					api.createIframeRange = function(iframeEl) {
						return api.createRange(dom.getIframeDocument(iframeEl));
					};

					api.createIframeRangyRange = function(iframeEl) {
						return api.createRangyRange(dom
								.getIframeDocument(iframeEl));
					};

					api.addCreateMissingNativeApiListener(function(win) {
						var doc = win.document;
						if (typeof doc.createRange == "undefined") {
							doc.createRange = function() {
								return api.createRange(this);
							};
						}
						doc = win = null;
					});
				});
rangy
		.createModule(
				"WrappedSelection",
				function(api, module) {
					// This will create a selection object wrapper that follows
					// the Selection object found in the WHATWG draft DOM Range
					// spec (http://html5.org/specs/dom-range.html)

					api
							.requireModules([ "DomUtil", "DomRange",
									"WrappedRange" ]);

					api.config.checkSelectionRanges = true;

					var BOOLEAN = "boolean", windowPropertyName = "_rangySelection", dom = api.dom, util = api.util, DomRange = api.DomRange, WrappedRange = api.WrappedRange, DOMException = api.DOMException, DomPosition = dom.DomPosition, getSelection, selectionIsCollapsed, CONTROL = "Control";

					function getWinSelection(winParam) {
						return (winParam || window).getSelection();
					}

					function getDocSelection(winParam) {
						return (winParam || window).document.selection;
					}

					// Test for the Range/TextRange and Selection features
					// required
					// Test for ability to retrieve selection
					var implementsWinGetSelection = api.util.isHostMethod(
							window, "getSelection"), implementsDocSelection = api.util
							.isHostObject(document, "selection");

					var useDocumentSelection = implementsDocSelection
							&& (!implementsWinGetSelection || api.config.preferTextRange);

					if (useDocumentSelection) {
						getSelection = getDocSelection;
						api.isSelectionValid = function(winParam) {
							var doc = (winParam || window).document, nativeSel = doc.selection;

							// Check whether the selection TextRange is actually
							// contained within the correct document
							return (nativeSel.type != "None" || dom
									.getDocument(nativeSel.createRange()
											.parentElement()) == doc);
						};
					} else if (implementsWinGetSelection) {
						getSelection = getWinSelection;
						api.isSelectionValid = function() {
							return true;
						};
					} else {
						module
								.fail("Neither document.selection or window.getSelection() detected.");
					}

					api.getNativeSelection = getSelection;

					var testSelection = getSelection();
					var testRange = api.createNativeRange(document);
					var body = dom.getBody(document);

					// Obtaining a range from a selection
					var selectionHasAnchorAndFocus = util.areHostObjects(
							testSelection, [ "anchorNode", "focusNode" ]
									&& util.areHostProperties(testSelection, [
											"anchorOffset", "focusOffset" ]));
					api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;

					// Test for existence of native selection extend() method
					var selectionHasExtend = util.isHostMethod(testSelection,
							"extend");
					api.features.selectionHasExtend = selectionHasExtend;

					// Test if rangeCount exists
					var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
					api.features.selectionHasRangeCount = selectionHasRangeCount;

					var selectionSupportsMultipleRanges = false;
					var collapsedNonEditableSelectionsSupported = true;

					if (util.areHostMethods(testSelection, [ "addRange",
							"getRangeAt", "removeAllRanges" ])
							&& typeof testSelection.rangeCount == "number"
							&& api.features.implementsDomRange) {

						(function() {
							var iframe = document.createElement("iframe");
							body.appendChild(iframe);

							var iframeDoc = dom.getIframeDocument(iframe);
							iframeDoc.open();
							iframeDoc
									.write("<html><head></head><body>12</body></html>");
							iframeDoc.close();

							var sel = dom.getIframeWindow(iframe)
									.getSelection();
							var docEl = iframeDoc.documentElement;
							var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;

							// Test whether the native selection will allow a
							// collapsed selection within a non-editable element
							var r1 = iframeDoc.createRange();
							r1.setStart(textNode, 1);
							r1.collapse(true);
							sel.addRange(r1);
							collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
							sel.removeAllRanges();

							// Test whether the native selection is capable of
							// supporting multiple ranges
							var r2 = r1.cloneRange();
							r1.setStart(textNode, 0);
							r2.setEnd(textNode, 2);
							sel.addRange(r1);
							sel.addRange(r2);

							selectionSupportsMultipleRanges = (sel.rangeCount == 2);

							// Clean up
							r1.detach();
							r2.detach();

							body.removeChild(iframe);
						})();
					}

					api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
					api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;

					// ControlRanges
					var implementsControlRange = false, testControlRange;

					if (body && util.isHostMethod(body, "createControlRange")) {
						testControlRange = body.createControlRange();
						if (util.areHostProperties(testControlRange, [ "item",
								"add" ])) {
							implementsControlRange = true;
						}
					}
					api.features.implementsControlRange = implementsControlRange;

					// Selection collapsedness
					if (selectionHasAnchorAndFocus) {
						selectionIsCollapsed = function(sel) {
							return sel.anchorNode === sel.focusNode
									&& sel.anchorOffset === sel.focusOffset;
						};
					} else {
						selectionIsCollapsed = function(sel) {
							return sel.rangeCount ? sel
									.getRangeAt(sel.rangeCount - 1).collapsed
									: false;
						};
					}

					function updateAnchorAndFocusFromRange(sel, range,
							backwards) {
						var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start"
								: "end";
						sel.anchorNode = range[anchorPrefix + "Container"];
						sel.anchorOffset = range[anchorPrefix + "Offset"];
						sel.focusNode = range[focusPrefix + "Container"];
						sel.focusOffset = range[focusPrefix + "Offset"];
					}

					function updateAnchorAndFocusFromNativeSelection(sel) {
						var nativeSel = sel.nativeSelection;
						sel.anchorNode = nativeSel.anchorNode;
						sel.anchorOffset = nativeSel.anchorOffset;
						sel.focusNode = nativeSel.focusNode;
						sel.focusOffset = nativeSel.focusOffset;
					}

					function updateEmptySelection(sel) {
						sel.anchorNode = sel.focusNode = null;
						sel.anchorOffset = sel.focusOffset = 0;
						sel.rangeCount = 0;
						sel.isCollapsed = true;
						sel._ranges.length = 0;
					}

					function getNativeRange(range) {
						var nativeRange;
						if (range instanceof DomRange) {
							nativeRange = range._selectionNativeRange;
							if (!nativeRange) {
								nativeRange = api.createNativeRange(dom
										.getDocument(range.startContainer));
								nativeRange.setEnd(range.endContainer,
										range.endOffset);
								nativeRange.setStart(range.startContainer,
										range.startOffset);
								range._selectionNativeRange = nativeRange;
								range.attachListener("detach", function() {

									this._selectionNativeRange = null;
								});
							}
						} else if (range instanceof WrappedRange) {
							nativeRange = range.nativeRange;
						} else if (api.features.implementsDomRange
								&& (range instanceof dom
										.getWindow(range.startContainer).Range)) {
							nativeRange = range;
						}
						return nativeRange;
					}

					function rangeContainsSingleElement(rangeNodes) {
						if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
							return false;
						}
						for ( var i = 1, len = rangeNodes.length; i < len; ++i) {
							if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
								return false;
							}
						}
						return true;
					}

					function getSingleElementFromRange(range) {
						var nodes = range.getNodes();
						if (!rangeContainsSingleElement(nodes)) {
							throw new Error("getSingleElementFromRange: range "
									+ range.inspect()
									+ " did not consist of a single element");
						}
						return nodes[0];
					}

					function isTextRange(range) {
						return !!range && typeof range.text != "undefined";
					}

					function updateFromTextRange(sel, range) {
						// Create a Range from the selected TextRange
						var wrappedRange = new WrappedRange(range);
						sel._ranges = [ wrappedRange ];

						updateAnchorAndFocusFromRange(sel, wrappedRange, false);
						sel.rangeCount = 1;
						sel.isCollapsed = wrappedRange.collapsed;
					}

					function updateControlSelection(sel) {
						// Update the wrapped selection based on what's now in
						// the native selection
						sel._ranges.length = 0;
						if (sel.docSelection.type == "None") {
							updateEmptySelection(sel);
						} else {
							var controlRange = sel.docSelection.createRange();
							if (isTextRange(controlRange)) {
								// This case (where the selection type is
								// "Control" and calling createRange() on the
								// selection returns
								// a TextRange) can happen in IE 9. It happens,
								// for example, when all elements in the
								// selected
								// ControlRange have been removed from the
								// ControlRange and removed from the document.
								updateFromTextRange(sel, controlRange);
							} else {
								sel.rangeCount = controlRange.length;
								var range, doc = dom.getDocument(controlRange
										.item(0));
								for ( var i = 0; i < sel.rangeCount; ++i) {
									range = api.createRange(doc);
									range.selectNode(controlRange.item(i));
									sel._ranges.push(range);
								}
								sel.isCollapsed = sel.rangeCount == 1
										&& sel._ranges[0].collapsed;
								updateAnchorAndFocusFromRange(sel,
										sel._ranges[sel.rangeCount - 1], false);
							}
						}
					}

					function addRangeToControlSelection(sel, range) {
						var controlRange = sel.docSelection.createRange();
						var rangeElement = getSingleElementFromRange(range);

						// Create a new ControlRange containing all the elements
						// in the selected ControlRange plus the element
						// contained by the supplied range
						var doc = dom.getDocument(controlRange.item(0));
						var newControlRange = dom.getBody(doc)
								.createControlRange();
						for ( var i = 0, len = controlRange.length; i < len; ++i) {
							newControlRange.add(controlRange.item(i));
						}
						try {
							newControlRange.add(rangeElement);
						} catch (ex) {
							throw new Error(
									"addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
						}
						newControlRange.select();

						// Update the wrapped selection based on what's now in
						// the native selection
						updateControlSelection(sel);
					}

					var getSelectionRangeAt;

					if (util.isHostMethod(testSelection, "getRangeAt")) {
						getSelectionRangeAt = function(sel, index) {
							try {
								return sel.getRangeAt(index);
							} catch (ex) {
								return null;
							}
						};
					} else if (selectionHasAnchorAndFocus) {
						getSelectionRangeAt = function(sel) {
							var doc = dom.getDocument(sel.anchorNode);
							var range = api.createRange(doc);
							range.setStart(sel.anchorNode, sel.anchorOffset);
							range.setEnd(sel.focusNode, sel.focusOffset);

							// Handle the case when the selection was selected
							// backwards (from the end to the start in the
							// document)
							if (range.collapsed !== this.isCollapsed) {
								range.setStart(sel.focusNode, sel.focusOffset);
								range.setEnd(sel.anchorNode, sel.anchorOffset);
							}

							return range;
						};
					}

					/**
					 * @constructor
					 */
					function WrappedSelection(selection, docSelection, win) {
						this.nativeSelection = selection;
						this.docSelection = docSelection;
						this._ranges = [];
						this.win = win;
						this.refresh();
					}

					api.getSelection = function(win) {
						win = win || window;
						var sel = win[windowPropertyName];
						var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win)
								: null;
						if (sel) {
							sel.nativeSelection = nativeSel;
							sel.docSelection = docSel;
							sel.refresh(win);
						} else {
							sel = new WrappedSelection(nativeSel, docSel, win);
							win[windowPropertyName] = sel;
						}
						return sel;
					};

					api.getIframeSelection = function(iframeEl) {
						return api.getSelection(dom.getIframeWindow(iframeEl));
					};

					var selProto = WrappedSelection.prototype;

					function createControlSelection(sel, ranges) {
						// Ensure that the selection becomes of type "Control"
						var doc = dom.getDocument(ranges[0].startContainer);
						var controlRange = dom.getBody(doc)
								.createControlRange();
						for ( var i = 0, el; i < rangeCount; ++i) {
							el = getSingleElementFromRange(ranges[i]);
							try {
								controlRange.add(el);
							} catch (ex) {
								throw new Error(
										"setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
							}
						}
						controlRange.select();

						// Update the wrapped selection based on what's now in
						// the native selection
						updateControlSelection(sel);
					}

					// Selecting a range
					if (!useDocumentSelection
							&& selectionHasAnchorAndFocus
							&& util.areHostMethods(testSelection, [
									"removeAllRanges", "addRange" ])) {
						selProto.removeAllRanges = function() {
							this.nativeSelection.removeAllRanges();
							updateEmptySelection(this);
						};

						var addRangeBackwards = function(sel, range) {
							var doc = DomRange.getRangeDocument(range);
							var endRange = api.createRange(doc);
							endRange.collapseToPoint(range.endContainer,
									range.endOffset);
							sel.nativeSelection
									.addRange(getNativeRange(endRange));
							sel.nativeSelection.extend(range.startContainer,
									range.startOffset);
							sel.refresh();
						};

						if (selectionHasRangeCount) {
							selProto.addRange = function(range, backwards) {
								if (implementsControlRange
										&& implementsDocSelection
										&& this.docSelection.type == CONTROL) {
									addRangeToControlSelection(this, range);
								} else {
									if (backwards && selectionHasExtend) {
										addRangeBackwards(this, range);
									} else {
										var previousRangeCount;
										if (selectionSupportsMultipleRanges) {
											previousRangeCount = this.rangeCount;
										} else {
											this.removeAllRanges();
											previousRangeCount = 0;
										}
										this.nativeSelection
												.addRange(getNativeRange(range));

										// Check whether adding the range was
										// successful
										this.rangeCount = this.nativeSelection.rangeCount;

										if (this.rangeCount == previousRangeCount + 1) {
											// The range was added successfully

											// Check whether the range that we
											// added to the selection is
											// reflected in the last range
											// extracted from
											// the selection
											if (api.config.checkSelectionRanges) {
												var nativeRange = getSelectionRangeAt(
														this.nativeSelection,
														this.rangeCount - 1);
												if (nativeRange
														&& !DomRange
																.rangesEqual(
																		nativeRange,
																		range)) {
													// Happens in WebKit with,
													// for example, a selection
													// placed at the start of a
													// text node
													range = new WrappedRange(
															nativeRange);
												}
											}
											this._ranges[this.rangeCount - 1] = range;
											updateAnchorAndFocusFromRange(
													this,
													range,
													selectionIsBackwards(this.nativeSelection));
											this.isCollapsed = selectionIsCollapsed(this);
										} else {
											// The range was not added
											// successfully. The simplest thing
											// is to refresh
											this.refresh();
										}
									}
								}
							};
						} else {
							selProto.addRange = function(range, backwards) {
								if (backwards && selectionHasExtend) {
									addRangeBackwards(this, range);
								} else {
									this.nativeSelection
											.addRange(getNativeRange(range));
									this.refresh();
								}
							};
						}

						selProto.setRanges = function(ranges) {
							if (implementsControlRange && ranges.length > 1) {
								createControlSelection(this, ranges);
							} else {
								this.removeAllRanges();
								for ( var i = 0, len = ranges.length; i < len; ++i) {
									this.addRange(ranges[i]);
								}
							}
						};
					} else if (util.isHostMethod(testSelection, "empty")
							&& util.isHostMethod(testRange, "select")
							&& implementsControlRange && useDocumentSelection) {

						selProto.removeAllRanges = function() {
							// Added try/catch as fix for issue #21
							try {
								this.docSelection.empty();

								// Check for empty() not working (issue #24)
								if (this.docSelection.type != "None") {
									// Work around failure to empty a control
									// selection by instead selecting a
									// TextRange and then
									// calling empty()
									var doc;
									if (this.anchorNode) {
										doc = dom.getDocument(this.anchorNode);
									} else if (this.docSelection.type == CONTROL) {
										var controlRange = this.docSelection
												.createRange();
										if (controlRange.length) {
											doc = dom.getDocument(controlRange
													.item(0)).body
													.createTextRange();
										}
									}
									if (doc) {
										var textRange = doc.body
												.createTextRange();
										textRange.select();
										this.docSelection.empty();
									}
								}
							} catch (ex) {
							}
							updateEmptySelection(this);
						};

						selProto.addRange = function(range) {
							if (this.docSelection.type == CONTROL) {
								addRangeToControlSelection(this, range);
							} else {
								WrappedRange.rangeToTextRange(range).select();
								this._ranges[0] = range;
								this.rangeCount = 1;
								this.isCollapsed = this._ranges[0].collapsed;
								updateAnchorAndFocusFromRange(this, range,
										false);
							}
						};

						selProto.setRanges = function(ranges) {
							this.removeAllRanges();
							var rangeCount = ranges.length;
							if (rangeCount > 1) {
								createControlSelection(this, ranges);
							} else if (rangeCount) {
								this.addRange(ranges[0]);
							}
						};
					} else {
						module
								.fail("No means of selecting a Range or TextRange was found");
						return false;
					}

					selProto.getRangeAt = function(index) {
						if (index < 0 || index >= this.rangeCount) {
							throw new DOMException("INDEX_SIZE_ERR");
						} else {
							return this._ranges[index];
						}
					};

					var refreshSelection;

					if (useDocumentSelection) {
						refreshSelection = function(sel) {
							var range;
							if (api.isSelectionValid(sel.win)) {
								range = sel.docSelection.createRange();
							} else {
								range = dom.getBody(sel.win.document)
										.createTextRange();
								range.collapse(true);
							}

							if (sel.docSelection.type == CONTROL) {
								updateControlSelection(sel);
							} else if (isTextRange(range)) {
								updateFromTextRange(sel, range);
							} else {
								updateEmptySelection(sel);
							}
						};
					} else if (util.isHostMethod(testSelection, "getRangeAt")
							&& typeof testSelection.rangeCount == "number") {
						refreshSelection = function(sel) {
							if (implementsControlRange
									&& implementsDocSelection
									&& sel.docSelection.type == CONTROL) {
								updateControlSelection(sel);
							} else {
								sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
								if (sel.rangeCount) {
									for ( var i = 0, len = sel.rangeCount; i < len; ++i) {
										sel._ranges[i] = new api.WrappedRange(
												sel.nativeSelection
														.getRangeAt(i));
									}
									updateAnchorAndFocusFromRange(
											sel,
											sel._ranges[sel.rangeCount - 1],
											selectionIsBackwards(sel.nativeSelection));
									sel.isCollapsed = selectionIsCollapsed(sel);
								} else {
									updateEmptySelection(sel);
								}
							}
						};
					} else if (selectionHasAnchorAndFocus
							&& typeof testSelection.isCollapsed == BOOLEAN
							&& typeof testRange.collapsed == BOOLEAN
							&& api.features.implementsDomRange) {
						refreshSelection = function(sel) {
							var range, nativeSel = sel.nativeSelection;
							if (nativeSel.anchorNode) {
								range = getSelectionRangeAt(nativeSel, 0);
								sel._ranges = [ range ];
								sel.rangeCount = 1;
								updateAnchorAndFocusFromNativeSelection(sel);
								sel.isCollapsed = selectionIsCollapsed(sel);
							} else {
								updateEmptySelection(sel);
							}
						};
					} else {
						module
								.fail("No means of obtaining a Range or TextRange from the user's selection was found");
						return false;
					}

					selProto.refresh = function(checkForChanges) {
						var oldRanges = checkForChanges ? this._ranges.slice(0)
								: null;
						refreshSelection(this);
						if (checkForChanges) {
							var i = oldRanges.length;
							if (i != this._ranges.length) {
								return false;
							}
							while (i--) {
								if (!DomRange.rangesEqual(oldRanges[i],
										this._ranges[i])) {
									return false;
								}
							}
							return true;
						}
					};

					// Removal of a single range
					var removeRangeManually = function(sel, range) {
						var ranges = sel.getAllRanges(), removed = false;
						sel.removeAllRanges();
						for ( var i = 0, len = ranges.length; i < len; ++i) {
							if (removed || range !== ranges[i]) {
								sel.addRange(ranges[i]);
							} else {
								// According to the draft WHATWG Range spec, the
								// same range may be added to the selection
								// multiple
								// times. removeRange should only remove the
								// first instance, so the following ensures only
								// the first
								// instance is removed
								removed = true;
							}
						}
						if (!sel.rangeCount) {
							updateEmptySelection(sel);
						}
					};

					if (implementsControlRange) {
						selProto.removeRange = function(range) {
							if (this.docSelection.type == CONTROL) {
								var controlRange = this.docSelection
										.createRange();
								var rangeElement = getSingleElementFromRange(range);

								// Create a new ControlRange containing all the
								// elements in the selected ControlRange minus
								// the
								// element contained by the supplied range
								var doc = dom.getDocument(controlRange.item(0));
								var newControlRange = dom.getBody(doc)
										.createControlRange();
								var el, removed = false;
								for ( var i = 0, len = controlRange.length; i < len; ++i) {
									el = controlRange.item(i);
									if (el !== rangeElement || removed) {
										newControlRange.add(controlRange
												.item(i));
									} else {
										removed = true;
									}
								}
								newControlRange.select();

								// Update the wrapped selection based on what's
								// now in the native selection
								updateControlSelection(this);
							} else {
								removeRangeManually(this, range);
							}
						};
					} else {
						selProto.removeRange = function(range) {
							removeRangeManually(this, range);
						};
					}

					// Detecting if a selection is backwards
					var selectionIsBackwards;
					if (!useDocumentSelection && selectionHasAnchorAndFocus
							&& api.features.implementsDomRange) {
						selectionIsBackwards = function(sel) {
							var backwards = false;
							if (sel.anchorNode) {
								backwards = (dom.comparePoints(sel.anchorNode,
										sel.anchorOffset, sel.focusNode,
										sel.focusOffset) == 1);
							}
							return backwards;
						};

						selProto.isBackwards = function() {
							return selectionIsBackwards(this);
						};
					} else {
						selectionIsBackwards = selProto.isBackwards = function() {
							return false;
						};
					}

					// Selection text
					// This is conformant to the new WHATWG DOM Range draft spec
					// but differs from WebKit and Mozilla's implementation
					selProto.toString = function() {

						var rangeTexts = [];
						for ( var i = 0, len = this.rangeCount; i < len; ++i) {
							rangeTexts[i] = "" + this._ranges[i];
						}
						return rangeTexts.join("");
					};

					function assertNodeInSameDocument(sel, node) {
						if (sel.anchorNode
								&& (dom.getDocument(sel.anchorNode) !== dom
										.getDocument(node))) {
							throw new DOMException("WRONG_DOCUMENT_ERR");
						}
					}

					// No current browsers conform fully to the HTML 5 draft
					// spec for this method, so Rangy's own method is always
					// used
					selProto.collapse = function(node, offset) {
						assertNodeInSameDocument(this, node);
						var range = api.createRange(dom.getDocument(node));
						range.collapseToPoint(node, offset);
						this.removeAllRanges();
						this.addRange(range);
						this.isCollapsed = true;
					};

					selProto.collapseToStart = function() {
						if (this.rangeCount) {
							var range = this._ranges[0];
							this.collapse(range.startContainer,
									range.startOffset);
						} else {
							throw new DOMException("INVALID_STATE_ERR");
						}
					};

					selProto.collapseToEnd = function() {
						if (this.rangeCount) {
							var range = this._ranges[this.rangeCount - 1];
							this.collapse(range.endContainer, range.endOffset);
						} else {
							throw new DOMException("INVALID_STATE_ERR");
						}
					};

					// The HTML 5 spec is very specific on how selectAllChildren
					// should be implemented so the native implementation is
					// never used by Rangy.
					selProto.selectAllChildren = function(node) {
						assertNodeInSameDocument(this, node);
						var range = api.createRange(dom.getDocument(node));
						range.selectNodeContents(node);
						this.removeAllRanges();
						this.addRange(range);
					};

					selProto.deleteFromDocument = function() {
						// Sepcial behaviour required for Control selections
						if (implementsControlRange && implementsDocSelection
								&& this.docSelection.type == CONTROL) {
							var controlRange = this.docSelection.createRange();
							var element;
							while (controlRange.length) {
								element = controlRange.item(0);
								controlRange.remove(element);
								element.parentNode.removeChild(element);
							}
							this.refresh();
						} else if (this.rangeCount) {
							var ranges = this.getAllRanges();
							this.removeAllRanges();
							for ( var i = 0, len = ranges.length; i < len; ++i) {
								ranges[i].deleteContents();
							}
							// The HTML5 spec says nothing about what the
							// selection should contain after calling
							// deleteContents on each
							// range. Firefox moves the selection to where the
							// final selected range was, so we emulate that
							this.addRange(ranges[len - 1]);
						}
					};

					// The following are non-standard extensions
					selProto.getAllRanges = function() {
						return this._ranges.slice(0);
					};

					selProto.setSingleRange = function(range) {
						this.setRanges([ range ]);
					};

					selProto.containsNode = function(node, allowPartial) {
						for ( var i = 0, len = this._ranges.length; i < len; ++i) {
							if (this._ranges[i]
									.containsNode(node, allowPartial)) {
								return true;
							}
						}
						return false;
					};

					selProto.toHtml = function() {
						var html = "";
						if (this.rangeCount) {
							var container = DomRange.getRangeDocument(
									this._ranges[0]).createElement("div");
							for ( var i = 0, len = this._ranges.length; i < len; ++i) {
								container.appendChild(this._ranges[i]
										.cloneContents());
							}
							html = container.innerHTML;
						}
						return html;
					};

					function inspect(sel) {
						var rangeInspects = [];
						var anchor = new DomPosition(sel.anchorNode,
								sel.anchorOffset);
						var focus = new DomPosition(sel.focusNode,
								sel.focusOffset);
						var name = (typeof sel.getName == "function") ? sel
								.getName() : "Selection";

						if (typeof sel.rangeCount != "undefined") {
							for ( var i = 0, len = sel.rangeCount; i < len; ++i) {
								rangeInspects[i] = DomRange.inspect(sel
										.getRangeAt(i));
							}
						}
						return "[" + name + "(Ranges: "
								+ rangeInspects.join(", ") + ")(anchor: "
								+ anchor.inspect() + ", focus: "
								+ focus.inspect() + "]";

					}

					selProto.getName = function() {
						return "WrappedSelection";
					};

					selProto.inspect = function() {
						return inspect(this);
					};

					selProto.detach = function() {
						this.win[windowPropertyName] = null;
						this.win = this.anchorNode = this.focusNode = null;
					};

					WrappedSelection.inspect = inspect;

					api.Selection = WrappedSelection;

					api.selectionPrototype = selProto;

					api.addCreateMissingNativeApiListener(function(win) {
						if (typeof win.getSelection == "undefined") {
							win.getSelection = function() {
								return api.getSelection(this);
							};
						}
						win = null;
					});
				});
/*
 * Base.js, version 1.1a Copyright 2006-2010, Dean Edwards License:
 * http://www.opensource.org/licenses/mit-license.php
 */

var Base = function() {
	// dummy
};

Base.extend = function(_instance, _static) { // subclass
	var extend = Base.prototype.extend;

	// build the prototype
	Base._prototyping = true;
	var proto = new this;
	extend.call(proto, _instance);
	proto.base = function() {
		// call this method from any other method to invoke that method's
		// ancestor
	};
	delete Base._prototyping;

	// create the wrapper for the constructor function
	// var constructor = proto.constructor.valueOf(); //-dean
	var constructor = proto.constructor;
	var klass = proto.constructor = function() {
		if (!Base._prototyping) {
			if (this._constructing || this.constructor == klass) { // instantiation
				this._constructing = true;
				constructor.apply(this, arguments);
				delete this._constructing;
			} else if (arguments[0] != null) { // casting
				return (arguments[0].extend || extend)
						.call(arguments[0], proto);
			}
		}
	};

	// build the class interface
	klass.ancestor = this;
	klass.extend = this.extend;
	klass.forEach = this.forEach;
	klass.implement = this.implement;
	klass.prototype = proto;
	klass.toString = this.toString;
	klass.valueOf = function(type) {
		// return (type == "object") ? klass : constructor; //-dean
		return (type == "object") ? klass : constructor.valueOf();
	};
	extend.call(klass, _static);
	// class initialisation
	if (typeof klass.init == "function")
		klass.init();
	return klass;
};

Base.prototype = {
	extend : function(source, value) {
		if (arguments.length > 1) { // extending with a name/value pair
			var ancestor = this[source];
			if (ancestor && (typeof value == "function") && // overriding a
			// method?
			// the valueOf() comparison is to avoid circular references
			(!ancestor.valueOf || ancestor.valueOf() != value.valueOf())
					&& /\bbase\b/.test(value)) {
				// get the underlying method
				var method = value.valueOf();
				// override
				value = function() {
					var previous = this.base || Base.prototype.base;
					this.base = ancestor;
					var returnValue = method.apply(this, arguments);
					this.base = previous;
					return returnValue;
				};
				// point to the underlying method
				value.valueOf = function(type) {
					return (type == "object") ? value : method;
				};
				value.toString = Base.toString;
			}
			this[source] = value;
		} else if (source) { // extending with an object literal
			var extend = Base.prototype.extend;
			// if this object has a customised extend method then use it
			if (!Base._prototyping && typeof this != "function") {
				extend = this.extend || extend;
			}
			var proto = {
				toSource : null
			};
			// do the "toString" and other methods manually
			var hidden = [ "constructor", "toString", "valueOf" ];
			// if we are prototyping then include the constructor
			var i = Base._prototyping ? 0 : 1;
			while (key = hidden[i++]) {
				if (source[key] != proto[key]) {
					extend.call(this, key, source[key]);

				}
			}
			// copy each of the source object's properties to this object
			for ( var key in source) {
				if (!proto[key])
					extend.call(this, key, source[key]);
			}
		}
		return this;
	}
};

// initialise
Base = Base.extend({
	constructor : function() {
		this.extend(arguments[0]);
	}
}, {
	ancestor : Object,
	version : "1.1",

	forEach : function(object, block, context) {
		for ( var key in object) {
			if (this.prototype[key] === undefined) {
				block.call(context, object[key], key, object);
			}
		}
	},

	implement : function() {
		for ( var i = 0; i < arguments.length; i++) {
			if (typeof arguments[i] == "function") {
				// if it's a function, call it
				arguments[i](this.prototype);
			} else {
				// add the interface using the extend method
				this.prototype.extend(arguments[i]);
			}
		}
		return this;
	},

	toString : function() {
		return String(this.valueOf());
	}
});
/**
 * Detect browser support for specific features
 */
wysihtml5.browser = (function() {
	var userAgent = navigator.userAgent, testElement = document
			.createElement("div"),
	// Browser sniffing is unfortunately needed since some behaviors are
	// impossible to feature detect
	isIE = userAgent.indexOf("MSIE") !== -1
			&& userAgent.indexOf("Opera") === -1, isGecko = userAgent
			.indexOf("Gecko") !== -1
			&& userAgent.indexOf("KHTML") === -1, isWebKit = userAgent
			.indexOf("AppleWebKit/") !== -1, isChrome = userAgent
			.indexOf("Chrome/") !== -1, isOpera = userAgent.indexOf("Opera/") !== -1;

	function iosVersion(userAgent) {
		return ((/ipad|iphone|ipod/.test(userAgent) && userAgent
				.match(/ os (\d+).+? like mac os x/)) || [ , 0 ])[1];
	}

	return {
		// Static variable needed, publicly accessible, to be able override it
		// in unit tests
		USER_AGENT : userAgent,

		/**
		 * Exclude browsers that are not capable of displaying and handling
		 * contentEditable as desired: - iPhone, iPad (tested iOS 4.2.2) and
		 * Android (tested 2.2) refuse to make contentEditables focusable - IE <
		 * 8 create invalid markup and crash randomly from time to time
		 * 
		 * @return {Boolean}
		 */
		supported : function() {
			var userAgent = this.USER_AGENT.toLowerCase(),
			// Essential for making html elements editable
			hasContentEditableSupport = "contentEditable" in testElement,
			// Following methods are needed in order to interact with the
			// contentEditable area
			hasEditingApiSupport = document.execCommand
					&& document.queryCommandSupported
					&& document.queryCommandState,
			// document selector apis are only supported by IE 8+, Safari 4+,
			// Chrome and Firefox 3.5+
			hasQuerySelectorSupport = document.querySelector
					&& document.querySelectorAll,
			// contentEditable is unusable in mobile browsers (tested iOS 4.2.2,
			// Android 2.2, Opera Mobile, WebOS 3.05)
			isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5)
					|| userAgent.indexOf("opera mobi") !== -1
					|| userAgent.indexOf("hpwos/") !== -1;

			return hasContentEditableSupport && hasEditingApiSupport
					&& hasQuerySelectorSupport && !isIncompatibleMobileBrowser;
		},

		isTouchDevice : function() {
			return this.supportsEvent("touchmove");
		},

		isIos : function() {
			var userAgent = this.USER_AGENT.toLowerCase();
			return userAgent.indexOf("webkit") !== -1
					&& userAgent.indexOf("mobile") !== -1;
		},

		/**
		 * Whether the browser supports sandboxed iframes Currently only IE 6+
		 * offers such feature <iframe security="restricted">
		 * 
		 * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
		 * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
		 * 
		 * HTML5 sandboxed iframes are still buggy and their DOM is not
		 * reachable from the outside (except when using postMessage)
		 */
		supportsSandboxedIframes : function() {
			return isIE;
		},

		/**
		 * IE6+7 throw a mixed content warning when the src of an iframe is
		 * empty/unset or about:blank window.querySelector is implemented as of
		 * IE8
		 */
		throwsMixedContentWarningWhenIframeSrcIsEmpty : function() {
			return !("querySelector" in document);
		},

		/**
		 * Whether the caret is correctly displayed in contentEditable elements
		 * Firefox sometimes shows a huge caret in the beginning after focusing
		 */
		displaysCaretInEmptyContentEditableCorrectly : function() {
			return !isGecko;
		},

		/**
		 * Opera and IE are the only browsers who offer the css value in the
		 * original unit, thx to the currentStyle object All other browsers
		 * provide the computed style in px via window.getComputedStyle
		 */
		hasCurrentStyleProperty : function() {
			return "currentStyle" in testElement;
		},

		/**
		 * Whether the browser inserts a <br>
		 * when pressing enter in a contentEditable element
		 */
		insertsLineBreaksOnReturn : function() {
			return isGecko;
		},

		supportsPlaceholderAttributeOn : function(element) {
			return "placeholder" in element;
		},

		supportsEvent : function(eventName) {
			return "on" + eventName in testElement || (function() {
				testElement.setAttribute("on" + eventName, "return;");
				return typeof (testElement["on" + eventName]) === "function";
			})();
		},

		/**
		 * Opera doesn't correctly fire focus/blur events when clicking in- and
		 * outside of iframe
		 */
		supportsEventsInIframeCorrectly : function() {
			return !isOpera;
		},

		/**
		 * Chrome & Safari only fire the ondrop/ondragend/... events when the
		 * ondragover event is cancelled with event.preventDefault Firefox 3.6
		 * fires those events anyway, but the mozilla doc says that the
		 * dragover/dragenter event needs to be cancelled
		 */
		firesOnDropOnlyWhenOnDragOverIsCancelled : function() {
			return isWebKit || isGecko;
		},

		/**
		 * Whether the browser supports the event.dataTransfer property in a
		 * proper way
		 */
		supportsDataTransfer : function() {
			try {
				// Firefox doesn't support dataTransfer in a safe way, it
				// doesn't strip script code in the html payload (like Chrome
				// does)
				return isWebKit
						&& (window.Clipboard || window.DataTransfer).prototype.getData;
			} catch (e) {
				return false;
			}
		},

		/**
		 * Everything below IE9 doesn't know how to treat HTML5 tags
		 * 
		 * @param {Object}
		 *            context The document object on which to check HTML5
		 *            support
		 * 
		 * @example wysihtml5.browser.supportsHTML5Tags(document);
		 */
		supportsHTML5Tags : function(context) {
			var element = context.createElement("div"), html5 = "<article>foo</article>";
			element.innerHTML = html5;
			return element.innerHTML.toLowerCase() === html5;
		},

		/**
		 * Checks whether a document supports a certain queryCommand In
		 * particular, Opera needs a reference to a document that has a
		 * contentEditable in it's dom tree in oder to report correct results
		 * 
		 * @param {Object}
		 *            doc Document object on which to check for a query command
		 * @param {String}
		 *            command The query command to check for
		 * @return {Boolean}
		 * 
		 * @example wysihtml5.browser.supportsCommand(document, "bold");
		 */
		supportsCommand : (function() {
			// Following commands are supported but contain bugs in some
			// browsers
			var buggyCommands = {
				// formatBlock fails with some tags (eg. <blockquote>)
				"formatBlock" : isIE,
				// When inserting unordered or ordered lists in Firefox, Chrome
				// or Safari, the current selection or line gets
				// converted into a list (<ul><li>...</li></ul>,
				// <ol><li>...</li></ol>)
				// IE and Opera act a bit different here as they convert the
				// entire content of the current block element into a list
				"insertUnorderedList" : isIE || isOpera || isWebKit,
				"insertOrderedList" : isIE || isOpera || isWebKit
			};

			// Firefox throws errors for queryCommandSupported, so we have to
			// build up our own object of supported commands
			var supported = {
				"insertHTML" : isGecko
			};

			return function(doc, command) {
				var isBuggy = buggyCommands[command];
				if (!isBuggy) {
					// Firefox throws errors when invoking queryCommandSupported
					// or queryCommandEnabled
					try {
						return doc.queryCommandSupported(command);
					} catch (e1) {
					}

					try {
						return doc.queryCommandEnabled(command);
					} catch (e2) {
						return !!supported[command];
					}
				}
				return false;
			};
		})(),

		/**
		 * IE: URLs starting with: www., http://, https://, ftp://, gopher://,
		 * mailto:, new:, snews:, telnet:, wasis:, file://, nntp://, newsrc:,
		 * ldap://, ldaps://, outlook:, mic:// and url: will automatically be
		 * auto-linked when either the user inserts them via copy&paste or
		 * presses the space bar when the caret is directly after such an url.
		 * This behavior cannot easily be avoided in IE < 9 since the logic is
		 * hardcoded in the mshtml.dll (related blog post on msdn
		 * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
		 */
		doesAutoLinkingInContentEditable : function() {
			return isIE;
		},

		/**
		 * As stated above, IE auto links urls typed into contentEditable
		 * elements Since IE9 it's possible to prevent this behavior
		 */
		canDisableAutoLinking : function() {
			return this.supportsCommand(document, "AutoUrlDetect");
		},

		/**
		 * IE leaves an empty paragraph in the contentEditable element after
		 * clearing it Chrome/Safari sometimes an empty <div>
		 */
		clearsContentEditableCorrectly : function() {
			return isGecko || isOpera || isWebKit;
		},

		/**
		 * IE gives wrong results for getAttribute
		 */
		supportsGetAttributeCorrectly : function() {
			var td = document.createElement("td");
			return td.getAttribute("rowspan") != "1";
		},

		/**
		 * When clicking on images in IE, Opera and Firefox, they are selected,
		 * which makes it easy to interact with them. Chrome and Safari both
		 * don't support this
		 */
		canSelectImagesInContentEditable : function() {
			return isGecko || isIE || isOpera;
		},

		/**
		 * When the caret is in an empty list (
		 * <ul>
		 * <li>|</li>
		 * </ul>) which is the first child in an contentEditable container
		 * pressing backspace doesn't remove the entire list as done in other
		 * browsers
		 */
		clearsListsInContentEditableCorrectly : function() {
			return isGecko || isIE || isWebKit;
		},

		/**
		 * All browsers except Safari and Chrome automatically scroll the
		 * range/caret position into view
		 */
		autoScrollsToCaret : function() {
			return !isWebKit;
		},

		/**
		 * Check whether the browser automatically closes tags that don't need
		 * to be opened
		 */
		autoClosesUnclosedTags : function() {
			var clonedTestElement = testElement.cloneNode(false), returnValue, innerHTML;

			clonedTestElement.innerHTML = "<p><div></div>";
			innerHTML = clonedTestElement.innerHTML.toLowerCase();
			returnValue = innerHTML === "<p></p><div></div>"
					|| innerHTML === "<p><div></div></p>";

			// Cache result by overwriting current function
			this.autoClosesUnclosedTags = function() {
				return returnValue;
			};

			return returnValue;
		},

		/**
		 * Whether the browser supports the native
		 * document.getElementsByClassName which returns live NodeLists
		 */
		supportsNativeGetElementsByClassName : function() {
			return String(document.getElementsByClassName).indexOf(
					"[native code]") !== -1;
		},

		/**
		 * As of now (19.04.2011) only supported by Firefox 4 and Chrome See
		 * https://developer.mozilla.org/en/DOM/Selection/modify
		 */
		supportsSelectionModify : function() {
			return "getSelection" in window
					&& "modify" in window.getSelection();
		},

		/**
		 * Whether the browser supports the classList object for fast className
		 * manipulation See
		 * https://developer.mozilla.org/en/DOM/element.classList
		 */
		supportsClassList : function() {
			return "classList" in testElement;
		},

		/**
		 * Opera needs a white space after a <br>
		 * in order to position the caret correctly
		 */
		needsSpaceAfterLineBreak : function() {
			return isOpera;
		},

		/**
		 * Whether the browser supports the speech api on the given element See
		 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
		 * 
		 * @example var input = document.createElement("input"); if
		 *          (wysihtml5.browser.supportsSpeechApiOn(input)) { // ... }
		 */
		supportsSpeechApiOn : function(input) {
			var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [ , 0 ];
			return chromeVersion[1] >= 11
					&& ("onwebkitspeechchange" in input || "speech" in input);
		},

		/**
		 * IE9 crashes when setting a getter via Object.defineProperty on
		 * XMLHttpRequest or XDomainRequest See
		 * https://connect.microsoft.com/ie/feedback/details/650112 or try the
		 * POC http://tifftiff.de/ie9_crash/
		 */
		crashesWhenDefineProperty : function(property) {
			return isIE
					&& (property === "XMLHttpRequest" || property === "XDomainRequest");
		},

		/**
		 * IE is the only browser who fires the "focus" event not immediately
		 * when .focus() is called on an element
		 */
		doesAsyncFocus : function() {
			return isIE;
		},

		/**
		 * In IE it's impssible for the user and for the selection library to
		 * set the caret after an <img> when it's the lastChild in the document
		 */
		hasProblemsSettingCaretAfterImg : function() {
			return isIE;
		},

		hasUndoInContextMenu : function() {
			return isGecko || isChrome || isOpera;
		}
	};
})();
wysihtml5.lang.array = function(arr) {
	return {
		/**
		 * Check whether a given object exists in an array
		 * 
		 * @example wysihtml5.lang.array([1, 2]).contains(1); // => true
		 */
		contains : function(needle) {
			if (arr.indexOf) {
				return arr.indexOf(needle) !== -1;
			} else {
				for ( var i = 0, length = arr.length; i < length; i++) {
					if (arr[i] === needle) {
						return true;
					}
				}
				return false;
			}
		},

		/**
		 * Substract one array from another
		 * 
		 * @example wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]); // =>
		 *          [1, 2]
		 */
		without : function(arrayToSubstract) {
			arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
			var newArr = [], i = 0, length = arr.length;
			for (; i < length; i++) {
				if (!arrayToSubstract.contains(arr[i])) {
					newArr.push(arr[i]);
				}
			}
			return newArr;
		},

		/**
		 * Return a clean native array
		 * 
		 * Following will convert a Live NodeList to a proper Array
		 * 
		 * @example var childNodes =
		 *          wysihtml5.lang.array(document.body.childNodes).get();
		 */
		get : function() {
			var i = 0, length = arr.length, newArray = [];
			for (; i < length; i++) {
				newArray.push(arr[i]);
			}
			return newArray;
		}
	};
};
wysihtml5.lang.Dispatcher = Base.extend(
/** @scope wysihtml5.lang.Dialog.prototype */
{
	observe : function(eventName, handler) {
		this.events = this.events || {};
		this.events[eventName] = this.events[eventName] || [];
		this.events[eventName].push(handler);
		return this;
	},

	on : function() {
		return this.observe.apply(this, wysihtml5.lang.array(arguments).get());
	},

	fire : function(eventName, payload) {
		this.events = this.events || {};
		var handlers = this.events[eventName] || [], i = 0;
		for (; i < handlers.length; i++) {
			handlers[i].call(this, payload);
		}
		return this;
	},

	stopObserving : function(eventName, handler) {
		this.events = this.events || {};
		var i = 0, handlers, newHandlers;
		if (eventName) {
			handlers = this.events[eventName] || [], newHandlers = [];
			for (; i < handlers.length; i++) {
				if (handlers[i] !== handler && handler) {
					newHandlers.push(handlers[i]);
				}
			}
			this.events[eventName] = newHandlers;
		} else {
			// Clean up all events
			this.events = {};
		}
		return this;
	}
});
wysihtml5.lang.object = function(obj) {
	return {
		/**
		 * @example wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2,
		 *          baz: 3 }).get(); // => { foo: 1, bar: 2, baz: 3 }
		 */
		merge : function(otherObj) {
			for ( var i in otherObj) {
				obj[i] = otherObj[i];
			}
			return this;
		},

		get : function() {
			return obj;
		},

		/**
		 * @example wysihtml5.lang.object({ foo: 1 }).clone(); // => { foo: 1 }
		 */
		clone : function() {
			var newObj = {}, i;
			for (i in obj) {
				newObj[i] = obj[i];
			}
			return newObj;
		},

		/**
		 * @example wysihtml5.lang.object([]).isArray(); // => true
		 */
		isArray : function() {
			return Object.prototype.toString.call(obj) === "[object Array]";
		}
	};
};
(function() {
	var WHITE_SPACE_START = /^\s+/, WHITE_SPACE_END = /\s+$/;
	wysihtml5.lang.string = function(str) {
		str = String(str);
		return {
			/**
			 * @example wysihtml5.lang.string(" foo ").trim(); // => "foo"
			 */
			trim : function() {
				return str.replace(WHITE_SPACE_START, "").replace(
						WHITE_SPACE_END, "");
			},

			/**
			 * @example wysihtml5.lang.string("Hello #{name}").interpolate({
			 *          name: "Christopher" }); // => "Hello Christopher"
			 */
			interpolate : function(vars) {
				for ( var i in vars) {
					str = this.replace("#{" + i + "}").by(vars[i]);
				}
				return str;
			},

			/**
			 * @example wysihtml5.lang.string("Hello
			 *          Tom").replace("Tom").with("Hans"); // => "Hello Hans"
			 */
			replace : function(search) {
				return {
					by : function(replace) {
						return str.split(search).join(replace);
					}
				}
			}
		};
	};
})();
/**
 * Find urls in descendant text nodes of an element and auto-links them Inspired
 * by
 * http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
 * 
 * @param {Element}
 *            element Container element in which to search for urls
 * 
 * @example <div id="text-container">Please click here: www.google.com</div>
 *          <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
 */
(function(wysihtml5) {
	var /**
		 * Don't auto-link urls that are contained in the following elements:
		 */
	IGNORE_URLS_IN = wysihtml5.lang.array([ "CODE", "PRE", "A", "SCRIPT",
			"HEAD", "TITLE", "STYLE" ]),
	/**
	 * revision 1: /(\S+\.{1}[^\s\,\.\!]+)/g
	 * 
	 * revision 2:
	 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
	 * 
	 * put this in the beginning if you don't wan't to match within a word
	 * (^|[\>\(\{\[\s\>])
	 */
	URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, MAX_DISPLAY_LENGTH = 100, BRACKETS = {
		")" : "(",
		"]" : "[",
		"}" : "{"
	};

	function autoLink(element) {
		if (_hasParentThatShouldBeIgnored(element)) {
			return element;
		}

		if (element === element.ownerDocument.documentElement) {
			element = element.ownerDocument.body;
		}

		return _parseNode(element);
	}

	/**
	 * This is basically a rebuild of the rails auto_link_urls text helper
	 */
	function _convertUrlsToLinks(str) {
		return str
				.replace(
						URL_REG_EXP,
						function(match, url) {
							var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1]
									|| "", opening = BRACKETS[punctuation];
							url = url.replace(TRAILING_CHAR_REG_EXP, "");

							if (url.split(opening).length > url
									.split(punctuation).length) {
								url = url + punctuation;
								punctuation = "";
							}
							var realUrl = url, displayUrl = url;
							if (url.length > MAX_DISPLAY_LENGTH) {
								displayUrl = displayUrl.substr(0,
										MAX_DISPLAY_LENGTH)
										+ "...";
							}
							// Add http prefix if necessary
							if (realUrl.substr(0, 4) === "www.") {
								realUrl = "http://" + realUrl;
							}

							return '<a href="' + realUrl + '">' + displayUrl
									+ '</a>' + punctuation;
						});
	}

	/**
	 * Creates or (if already cached) returns a temp element for the given
	 * document object
	 */
	function _getTempElement(context) {
		var tempElement = context._wysihtml5_tempElement;
		if (!tempElement) {
			tempElement = context._wysihtml5_tempElement = context
					.createElement("div");
		}
		return tempElement;
	}

	/**
	 * Replaces the original text nodes with the newly auto-linked dom tree
	 */
	function _wrapMatchesInNode(textNode) {
		var parentNode = textNode.parentNode, tempElement = _getTempElement(parentNode.ownerDocument);

		// We need to insert an empty/temporary <span /> to fix IE quirks
		// Elsewise IE would strip white space in the beginning
		tempElement.innerHTML = "<span></span>"
				+ _convertUrlsToLinks(textNode.data);
		tempElement.removeChild(tempElement.firstChild);

		while (tempElement.firstChild) {
			// inserts tempElement.firstChild before textNode
			parentNode.insertBefore(tempElement.firstChild, textNode);
		}
		parentNode.removeChild(textNode);
	}

	function _hasParentThatShouldBeIgnored(node) {
		var nodeName;
		while (node.parentNode) {
			node = node.parentNode;
			nodeName = node.nodeName;
			if (IGNORE_URLS_IN.contains(nodeName)) {
				return true;
			} else if (nodeName === "body") {
				return false;
			}
		}
		return false;
	}

	function _parseNode(element) {
		if (IGNORE_URLS_IN.contains(element.nodeName)) {
			return;
		}

		if (element.nodeType === wysihtml5.TEXT_NODE
				&& element.data.match(URL_REG_EXP)) {
			_wrapMatchesInNode(element);
			return;
		}

		var childNodes = wysihtml5.lang.array(element.childNodes).get(), childNodesLength = childNodes.length, i = 0;

		for (; i < childNodesLength; i++) {
			_parseNode(childNodes[i]);
		}

		return element;
	}

	wysihtml5.dom.autoLink = autoLink;

	// Reveal url reg exp to the outside
	wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
})(wysihtml5);
(function(wysihtml5) {
	var supportsClassList = wysihtml5.browser.supportsClassList(), api = wysihtml5.dom;

	api.addClass = function(element, className) {
		if (supportsClassList) {
			return element.classList.add(className);
		}
		if (api.hasClass(element, className)) {
			return;
		}
		element.className += " " + className;
	};

	api.removeClass = function(element, className) {
		if (supportsClassList) {
			return element.classList.remove(className);
		}

		element.className = element.className.replace(new RegExp("(^|\\s+)"
				+ className + "(\\s+|$)"), " ");
	};

	api.hasClass = function(element, className) {
		if (supportsClassList) {
			return element.classList.contains(className);
		}

		var elementClassName = element.className;
		return (elementClassName.length > 0 && (elementClassName == className || new RegExp(
				"(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
	};
})(wysihtml5);
wysihtml5.dom.contains = (function() {
	var documentElement = document.documentElement;
	if (documentElement.contains) {
		return function(container, element) {
			if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
				element = element.parentNode;
			}
			return container !== element && container.contains(element);
		};
	} else if (documentElement.compareDocumentPosition) {
		return function(container, element) {
			// https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
			return !!(container.compareDocumentPosition(element) & 16);
		};
	}
})();
/**
 * Converts an HTML fragment/element into a unordered/ordered list
 * 
 * @param {Element}
 *            element The element which should be turned into a list
 * @param {String}
 *            listType The list type in which to convert the tree (either "ul"
 *            or "ol")
 * @return {Element} The created list
 * 
 * @example <!-- Assume the following dom: --> <span id="pseudo-list"> eminem<br>
 *          dr. dre <div>50 Cent</div> </span>
 * 
 * <script> wysihtml5.dom.convertToList(document.getElementById("pseudo-list"),
 * "ul"); </script>
 * 
 * <!-- Will result in: -->
 * <ul>
 * <li>eminem</li>
 * <li>dr. dre</li>
 * <li>50 Cent</li>
 * </ul>
 */
wysihtml5.dom.convertToList = (function() {
	function _createListItem(doc, list) {
		var listItem = doc.createElement("li");
		list.appendChild(listItem);
		return listItem;
	}

	function _createList(doc, type) {
		return doc.createElement(type);
	}

	function convertToList(element, listType) {
		if (element.nodeName === "UL" || element.nodeName === "OL"
				|| element.nodeName === "MENU") {
			// Already a list
			return element;
		}

		var doc = element.ownerDocument, list = _createList(doc, listType), lineBreaks = element
				.querySelectorAll("br"), lineBreaksLength = lineBreaks.length, childNodes, childNodesLength, childNode, lineBreak, parentNode, isBlockElement, isLineBreak, currentListItem, i;

		// First find <br> at the end of inline elements and move them behind
		// them
		for (i = 0; i < lineBreaksLength; i++) {
			lineBreak = lineBreaks[i];
			while ((parentNode = lineBreak.parentNode)
					&& parentNode !== element
					&& parentNode.lastChild === lineBreak) {
				if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {
					parentNode.removeChild(lineBreak);
					break;
				}
				wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
			}
		}

		childNodes = wysihtml5.lang.array(element.childNodes).get();
		childNodesLength = childNodes.length;

		for (i = 0; i < childNodesLength; i++) {
			currentListItem = currentListItem || _createListItem(doc, list);
			childNode = childNodes[i];
			isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block";
			isLineBreak = childNode.nodeName === "BR";

			if (isBlockElement) {
				// Append blockElement to current <li> if empty, otherwise
				// create a new one
				currentListItem = currentListItem.firstChild ? _createListItem(
						doc, list) : currentListItem;
				currentListItem.appendChild(childNode);
				currentListItem = null;
				continue;
			}

			if (isLineBreak) {
				// Only create a new list item in the next iteration when the
				// current one has already content
				currentListItem = currentListItem.firstChild ? null
						: currentListItem;
				continue;
			}

			currentListItem.appendChild(childNode);
		}

		element.parentNode.replaceChild(list, element);
		return list;
	}

	return convertToList;
})();
/**
 * Copy a set of attributes from one element to another
 * 
 * @param {Array}
 *            attributesToCopy List of attributes which should be copied
 * @return {Object} Returns an object which offers the "from" method which can
 *         be invoked with the element where to copy the attributes from., this
 *         again returns an object which provides a method named "to" which can
 *         be invoked with the element where to copy the attributes to (see
 *         example)
 * 
 * @example var textarea = document.querySelector("textarea"), div =
 *          document.querySelector("div[contenteditable=true]"), anotherDiv =
 *          document.querySelector("div.preview");
 *          wysihtml5.dom.copyAttributes(["spellcheck", "value",
 *          "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
 * 
 */
wysihtml5.dom.copyAttributes = function(attributesToCopy) {
	return {
		from : function(elementToCopyFrom) {
			return {
				to : function(elementToCopyTo) {
					var attribute, i = 0, length = attributesToCopy.length;
					for (; i < length; i++) {
						attribute = attributesToCopy[i];
						if (typeof (elementToCopyFrom[attribute]) !== "undefined"
								&& elementToCopyFrom[attribute] !== "") {
							elementToCopyTo[attribute] = elementToCopyFrom[attribute];
						}
					}
					return {
						andTo : arguments.callee
					};
				}
			};
		}
	};
};
/**
 * Copy a set of styles from one element to another Please note that this only
 * works properly across browsers when the element from which to copy the styles
 * is in the dom
 * 
 * Interesting article on how to copy styles
 * 
 * @param {Array}
 *            stylesToCopy List of styles which should be copied
 * @return {Object} Returns an object which offers the "from" method which can
 *         be invoked with the element where to copy the styles from., this
 *         again returns an object which provides a method named "to" which can
 *         be invoked with the element where to copy the styles to (see example)
 * 
 * @example var textarea = document.querySelector("textarea"), div =
 *          document.querySelector("div[contenteditable=true]"), anotherDiv =
 *          document.querySelector("div.preview");
 *          wysihtml5.dom.copyStyles(["overflow-y", "width",
 *          "height"]).from(textarea).to(div).andTo(anotherDiv);
 * 
 */
(function(dom) {

	/**
	 * Mozilla, WebKit and Opera recalculate the computed width when box-sizing:
	 * boder-box; is set So if an element has "width: 200px; -moz-box-sizing:
	 * border-box; border: 1px;" then its computed css width will be 198px
	 */
	var BOX_SIZING_PROPERTIES = [ "-webkit-box-sizing", "-moz-box-sizing",
			"-ms-box-sizing", "box-sizing" ];

	var shouldIgnoreBoxSizingBorderBox = function(element) {
		if (hasBoxSizingBorderBox(element)) {
			return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
		}
		return false;
	};

	var hasBoxSizingBorderBox = function(element) {
		var i = 0, length = BOX_SIZING_PROPERTIES.length;
		for (; i < length; i++) {
			if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
				return BOX_SIZING_PROPERTIES[i];
			}
		}
	};

	dom.copyStyles = function(stylesToCopy) {
		return {
			from : function(element) {
				if (shouldIgnoreBoxSizingBorderBox(element)) {
					stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(
							BOX_SIZING_PROPERTIES);
				}

				var cssText = "", length = stylesToCopy.length, i = 0, property;
				for (; i < length; i++) {
					property = stylesToCopy[i];
					cssText += property + ":"
							+ dom.getStyle(property).from(element) + ";";
				}

				return {
					to : function(element) {
						dom.setStyles(cssText).on(element);
						return {
							andTo : arguments.callee
						};
					}
				};
			}
		};
	};
})(wysihtml5.dom);
/**
 * Event Delegation
 * 
 * @example wysihtml5.dom.delegate(document.body, "a", "click", function() { //
 *          foo });
 */
(function(wysihtml5) {

	wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
		return wysihtml5.dom.observe(container, eventName, function(event) {
			var target = event.target, match = wysihtml5.lang.array(container
					.querySelectorAll(selector));

			while (target && target !== container) {
				if (match.contains(target)) {
					handler.call(target, event);
					break;
				}
				target = target.parentNode;
			}
		});
	};

})(wysihtml5);
/**
 * Returns the given html wrapped in a div element
 * 
 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...)
 * correctly when inserted via innerHTML
 * 
 * @param {String}
 *            html The html which should be wrapped in a dom element
 * @param {Obejct}
 *            [context] Document object of the context the html belongs to
 * 
 * @example wysihtml5.dom.getAsDom("<article>foo</article>");
 */
wysihtml5.dom.getAsDom = (function() {

	var _innerHTMLShiv = function(html, context) {
		var tempElement = context.createElement("div");
		tempElement.style.display = "none";
		context.body.appendChild(tempElement);
		// IE throws an exception when trying to insert <frameset></frameset>
		// via innerHTML
		try {
			tempElement.innerHTML = html;
		} catch (e) {
		}
		context.body.removeChild(tempElement);
		return tempElement;
	};

	/**
	 * Make sure IE supports HTML5 tags, which is accomplished by simply
	 * creating one instance of each element
	 */
	var _ensureHTML5Compatibility = function(context) {
		if (context._wysihtml5_supportsHTML5Tags) {
			return;
		}
		for ( var i = 0, length = HTML5_ELEMENTS.length; i < length; i++) {
			context.createElement(HTML5_ELEMENTS[i]);
		}
		context._wysihtml5_supportsHTML5Tags = true;
	};

	/**
	 * List of html5 tags taken from http://simon.html5.org/html5-elements
	 */
	var HTML5_ELEMENTS = [ "abbr", "article", "aside", "audio", "bdi",
			"canvas", "command", "datalist", "details", "figcaption", "figure",
			"footer", "header", "hgroup", "keygen", "mark", "meter", "nav",
			"output", "progress", "rp", "rt", "ruby", "svg", "section",
			"source", "summary", "time", "track", "video", "wbr" ];

	return function(html, context) {
		context = context || document;
		var tempElement;
		if (typeof (html) === "object" && html.nodeType) {
			tempElement = context.createElement("div");
			tempElement.appendChild(html);
		} else if (wysihtml5.browser.supportsHTML5Tags(context)) {
			tempElement = context.createElement("div");
			tempElement.innerHTML = html;
		} else {
			_ensureHTML5Compatibility(context);
			tempElement = _innerHTMLShiv(html, context);
		}
		return tempElement;
	};
})();
/**
 * Walks the dom tree from the given node up until it finds a match Designed for
 * optimal performance.
 * 
 * @param {Element}
 *            node The from which to check the parent nodes
 * @param {Object}
 *            matchingSet Object to match against (possible properties:
 *            nodeName, className, classRegExp)
 * @param {Number}
 *            [levels] How many parents should the function check up from the
 *            current node (defaults to 50)
 * @return {null|Element} Returns the first element that matched the
 *         desiredNodeName(s)
 * @example var listElement =
 *          wysihtml5.dom.getParentElement(document.querySelector("li"), {
 *          nodeName: ["MENU", "UL", "OL"] }); // ... or ... var
 *          unorderedListElement =
 *          wysihtml5.dom.getParentElement(document.querySelector("li"), {
 *          nodeName: "UL" }); // ... or ... var coloredElement =
 *          wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN",
 *          className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g
 *          });
 */
wysihtml5.dom.getParentElement = (function() {

	function _isSameNodeName(nodeName, desiredNodeNames) {
		if (!desiredNodeNames || !desiredNodeNames.length) {
			return true;
		}

		if (typeof (desiredNodeNames) === "string") {
			return nodeName === desiredNodeNames;
		} else {
			return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
		}
	}

	function _isElement(node) {
		return node.nodeType === wysihtml5.ELEMENT_NODE;
	}

	function _hasClassName(element, className, classRegExp) {
		var classNames = (element.className || "").match(classRegExp) || [];
		if (!className) {
			return !!classNames.length;
		}
		return classNames[classNames.length - 1] === className;
	}

	function _getParentElementWithNodeName(node, nodeName, levels) {
		while (levels-- && node && node.nodeName !== "BODY") {
			if (_isSameNodeName(node.nodeName, nodeName)) {
				return node;
			}
			node = node.parentNode;
		}
		return null;
	}

	function _getParentElementWithNodeNameAndClassName(node, nodeName,
			className, classRegExp, levels) {
		while (levels-- && node && node.nodeName !== "BODY") {
			if (_isElement(node) && _isSameNodeName(node.nodeName, nodeName)
					&& _hasClassName(node, className, classRegExp)) {
				return node;
			}
			node = node.parentNode;
		}
		return null;
	}

	return function(node, matchingSet, levels) {
		levels = levels || 50; // Go max 50 nodes upwards from current node
		if (matchingSet.className || matchingSet.classRegExp) {
			return _getParentElementWithNodeNameAndClassName(node,
					matchingSet.nodeName, matchingSet.className,
					matchingSet.classRegExp, levels);
		} else {
			return _getParentElementWithNodeName(node, matchingSet.nodeName,
					levels);
		}
	};
})();
/**
 * Get element's style for a specific css property
 * 
 * @param {Element}
 *            element The element on which to retrieve the style
 * @param {String}
 *            property The CSS property to retrieve ("float", "display",
 *            "text-align", ...)
 * 
 * @example wysihtml5.dom.getStyle("display").from(document.body); // => "block"
 */
wysihtml5.dom.getStyle = (function() {
	var stylePropertyMapping = {
		"float" : ("styleFloat" in document.createElement("div").style) ? "styleFloat"
				: "cssFloat"
	}, REG_EXP_CAMELIZE = /\-[a-z]/g;

	function camelize(str) {
		return str.replace(REG_EXP_CAMELIZE, function(match) {
			return match.charAt(1).toUpperCase();
		});
	}

	return function(property) {
		return {
			from : function(element) {
				if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
					return;
				}

				var doc = element.ownerDocument, camelizedProperty = stylePropertyMapping[property]
						|| camelize(property), style = element.style, currentStyle = element.currentStyle, styleValue = style[camelizedProperty];
				if (styleValue) {
					return styleValue;
				}

				// currentStyle is no standard and only supported by Opera and
				// IE but it has one important advantage over the
				// standard-compliant
				// window.getComputedStyle, since it returns css property values
				// in their original unit:
				// If you set an elements width to "50%",
				// window.getComputedStyle will give you it's current width in
				// px while currentStyle
				// gives you the original "50%".
				// Opera supports both, currentStyle and
				// window.getComputedStyle, that's why checking for currentStyle
				// should have higher prio
				if (currentStyle) {
					try {
						return currentStyle[camelizedProperty];
					} catch (e) {
						// ie will occasionally fail for unknown reasons.
						// swallowing exception
					}
				}

				var win = doc.defaultView || doc.parentWindow, needsOverflowReset = (property === "height" || property === "width")
						&& element.nodeName === "TEXTAREA", originalOverflow, returnValue;

				if (win.getComputedStyle) {
					// Chrome and Safari both calculate a wrong width and height
					// for textareas when they have scroll bars
					// therfore we remove and restore the scrollbar and
					// calculate the value in between
					if (needsOverflowReset) {
						originalOverflow = style.overflow;
						style.overflow = "hidden";
					}
					returnValue = win.getComputedStyle(element, null)
							.getPropertyValue(property);
					if (needsOverflowReset) {
						style.overflow = originalOverflow || "";
					}
					return returnValue;
				}
			}
		};
	};
})();
/**
 * High performant way to check whether an element with a specific tag name is
 * in the given document Optimized for being heavily executed Unleashes the
 * power of live node lists
 * 
 * @param {Object}
 *            doc The document object of the context where to check
 * @param {String}
 *            tagName Upper cased tag name
 * @example wysihtml5.dom.hasElementWithTagName(document, "IMG");
 */
wysihtml5.dom.hasElementWithTagName = (function() {
	var LIVE_CACHE = {}, DOCUMENT_IDENTIFIER = 1;

	function _getDocumentIdentifier(doc) {
		return doc._wysihtml5_identifier
				|| (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
	}

	return function(doc, tagName) {
		var key = _getDocumentIdentifier(doc) + ":" + tagName, cacheEntry = LIVE_CACHE[key];
		if (!cacheEntry) {
			cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
		}

		return cacheEntry.length > 0;
	};
})();
/**
 * High performant way to check whether an element with a specific class name is
 * in the given document Optimized for being heavily executed Unleashes the
 * power of live node lists
 * 
 * @param {Object}
 *            doc The document object of the context where to check
 * @param {String}
 *            tagName Upper cased tag name
 * @example wysihtml5.dom.hasElementWithClassName(document, "foobar");
 */
(function(wysihtml5) {
	var LIVE_CACHE = {}, DOCUMENT_IDENTIFIER = 1;

	function _getDocumentIdentifier(doc) {
		return doc._wysihtml5_identifier
				|| (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
	}

	wysihtml5.dom.hasElementWithClassName = function(doc, className) {
		// getElementsByClassName is not supported by IE<9
		// but is sometimes mocked via library code (which then doesn't return
		// live node lists)
		if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
			return !!doc.querySelector("." + className);
		}

		var key = _getDocumentIdentifier(doc) + ":" + className, cacheEntry = LIVE_CACHE[key];
		if (!cacheEntry) {
			cacheEntry = LIVE_CACHE[key] = doc
					.getElementsByClassName(className);
		}

		return cacheEntry.length > 0;
	};
})(wysihtml5);
wysihtml5.dom.insert = function(elementToInsert) {
	return {
		after : function(element) {
			element.parentNode.insertBefore(elementToInsert,
					element.nextSibling);
		},

		before : function(element) {
			element.parentNode.insertBefore(elementToInsert, element);
		},

		into : function(element) {
			element.appendChild(elementToInsert);
		}
	};
};
wysihtml5.dom.insertCSS = function(rules) {
	rules = rules.join("\n");

	return {
		into : function(doc) {
			var head = doc.head || doc.getElementsByTagName("head")[0], styleElement = doc
					.createElement("style");

			styleElement.type = "text/css";

			if (styleElement.styleSheet) {
				styleElement.styleSheet.cssText = rules;
			} else {
				styleElement.appendChild(doc.createTextNode(rules));
			}

			if (head) {
				head.appendChild(styleElement);
			}
		}
	};
};
/**
 * Method to set dom events
 * 
 * @example wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus",
 *          "blur"], function() { ... });
 */
wysihtml5.dom.observe = function(element, eventNames, handler) {
	eventNames = typeof (eventNames) === "string" ? [ eventNames ] : eventNames;

	var handlerWrapper, eventName, i = 0, length = eventNames.length;

	for (; i < length; i++) {
		eventName = eventNames[i];
		if (element.addEventListener) {
			element.addEventListener(eventName, handler, false);
		} else {
			handlerWrapper = function(event) {
				if (!("target" in event)) {
					event.target = event.srcElement;
				}
				event.preventDefault = event.preventDefault || function() {
					this.returnValue = false;
				};
				event.stopPropagation = event.stopPropagation || function() {
					this.cancelBubble = true;
				};
				handler.call(element, event);
			};
			element.attachEvent("on" + eventName, handlerWrapper);
		}
	}

	return {
		stop : function() {
			var eventName, i = 0, length = eventNames.length;
			for (; i < length; i++) {
				eventName = eventNames[i];
				if (element.removeEventListener) {
					element.removeEventListener(eventName, handler, false);
				} else {
					element.detachEvent("on" + eventName, handlerWrapper);
				}
			}
		}
	};
};
/**
 * HTML Sanitizer Rewrites the HTML based on given rules
 * 
 * @param {Element|String}
 *            elementOrHtml HTML String to be sanitized OR element whose content
 *            should be sanitized
 * @param {Object}
 *            [rules] List of rules for rewriting the HTML, if there's no rule
 *            for an element it will be converted to a "span". Each rule is a
 *            key/value pair where key is the tag to convert, and value the
 *            desired substitution.
 * @param {Object}
 *            context Document object in which to parse the html, needed to
 *            sandbox the parsing
 * 
 * @return {Element|String} Depends on the elementOrHtml parameter. When html
 *         then the sanitized html as string elsewise the element.
 * 
 * @example var userHTML = '<div id="foo" onclick="alert(1);">
 *          <p>
 *          <font color="red">foo</font><script>alert(1);</script>
 *          </p>
 *          </div>'; wysihtml5.dom.parse(userHTML, { tags { p: "div", // Rename
 *          p tags to div tags font: "span" // Rename font tags to span tags
 *          div: true, // Keep them, also possible (same result when passing:
 *          "div" or true) script: undefined // Remove script elements } }); // =>
 *          <div><div><span>foo bar</span></div></div>
 * 
 * var userHTML = '<table><tbody>
 * <tr>
 * <td>I'm a table!</td>
 * </tr>
 * </tbody></table>'; wysihtml5.dom.parse(userHTML); // => '<span><span><span><span>I'm
 * a table!</span></span></span></span>'
 * 
 * var userHTML = '<div>foobar<br>
 * foobar</div>'; wysihtml5.dom.parse(userHTML, { tags: { div: undefined, br:
 * true } }); // => ''
 * 
 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
 * wysihtml5.dom.parse(userHTML, { classes: { red: 1, green: 1 }, tags: { div: {
 * rename_tag: "p" } } }); // => '
 * <p class="red">
 * foo
 * </p>
 * <p>
 * bar
 * </p>'
 */
wysihtml5.dom.parse = (function() {

	/**
	 * It's not possible to use a XMLParser/DOMParser as HTML5 is not always
	 * well-formed XML new DOMParser().parseFromString('<img src="foo.gif">')
	 * will cause a parseError since the node isn't closed
	 * 
	 * Therefore we've to use the browser's ordinary HTML parser invoked by
	 * setting innerHTML.
	 */
	var NODE_TYPE_MAPPING = {
		"1" : _handleElement,
		"3" : _handleText
	},
	// Rename unknown tags to this
	DEFAULT_NODE_NAME = "span", WHITE_SPACE_REG_EXP = /\s+/, defaultRules = {
		tags : {},
		classes : {}
	}, currentRules = {};

	/**
	 * Iterates over all childs of the element, recreates them, appends them
	 * into a document fragment which later replaces the entire body content
	 */
	function parse(elementOrHtml, rules, context, cleanUp) {
		wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules)
				.get();

		context = context || elementOrHtml.ownerDocument || document;
		var fragment = context.createDocumentFragment(), isString = typeof (elementOrHtml) === "string", element, newNode, firstChild;

		if (isString) {
			element = wysihtml5.dom.getAsDom(elementOrHtml, context);
		} else {
			element = elementOrHtml;
		}

		while (element.firstChild) {
			firstChild = element.firstChild;
			element.removeChild(firstChild);
			newNode = _convert(firstChild, cleanUp);
			if (newNode) {
				fragment.appendChild(newNode);
			}
		}

		// Clear element contents
		element.innerHTML = "";

		// Insert new DOM tree
		element.appendChild(fragment);

		return isString ? wysihtml5.quirks.getCorrectInnerHTML(element)
				: element;
	}

	function _convert(oldNode, cleanUp) {
		var oldNodeType = oldNode.nodeType, oldChilds = oldNode.childNodes, oldChildsLength = oldChilds.length, newNode, method = NODE_TYPE_MAPPING[oldNodeType], i = 0;

		newNode = method && method(oldNode);

		if (!newNode) {
			return null;
		}

		for (i = 0; i < oldChildsLength; i++) {
			newChild = _convert(oldChilds[i], cleanUp);
			if (newChild) {
				newNode.appendChild(newChild);
			}
		}

		// Cleanup senseless <span> elements
		if (cleanUp && newNode.childNodes.length <= 1
				&& newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME
				&& !newNode.attributes.length) {
			return newNode.firstChild;
		}

		return newNode;
	}

	function _handleElement(oldNode) {
		var rule, newNode, endTag, tagRules = currentRules.tags, nodeName = oldNode.nodeName
				.toLowerCase(), scopeName = oldNode.scopeName;

		/**
		 * We already parsed that element ignore it! (yes, this sometimes
		 * happens in IE8 when the html is invalid)
		 */
		if (oldNode._wysihtml5) {
			return null;
		}
		oldNode._wysihtml5 = 1;

		if (oldNode.className === "wysihtml5-temp") {
			return null;
		}

		/**
		 * IE is the only browser who doesn't include the namespace in the
		 * nodeName, that's why we have to prepend it by ourselves scopeName is
		 * a proprietary IE feature read more here
		 * http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
		 */
		if (scopeName && scopeName != "HTML") {
			nodeName = scopeName + ":" + nodeName;
		}

		/**
		 * Repair node IE is a bit bitchy when it comes to invalid nested markup
		 * which includes unclosed tags A
		 * <p>
		 * doesn't need to be closed according HTML4-5 spec, we simply replace
		 * it with a <div> to preserve its content and layout
		 */
		if ("outerHTML" in oldNode) {
			if (!wysihtml5.browser.autoClosesUnclosedTags()
					&& oldNode.nodeName === "P"
					&& oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
				nodeName = "div";
			}
		}

		if (nodeName in tagRules) {
			rule = tagRules[nodeName];
			if (!rule || rule.remove) {
				return null;
			}

			rule = typeof (rule) === "string" ? {
				rename_tag : rule
			} : rule;
		} else if (oldNode.firstChild) {
			rule = {
				rename_tag : DEFAULT_NODE_NAME
			};
		} else {
			// Remove empty unknown elements
			return null;
		}

		newNode = oldNode.ownerDocument.createElement(rule.rename_tag
				|| nodeName);
		_handleAttributes(oldNode, newNode, rule);

		oldNode = null;
		return newNode;
	}

	function _handleAttributes(oldNode, newNode, rule) {
		var attributes = {}, // fresh new set of attributes to set on newNode
		setClass = rule.set_class, // classes to set
		addClass = rule.add_class, // add classes based on existing attributes
		setAttributes = rule.set_attributes, // attributes to set on the
		// current node
		checkAttributes = rule.check_attributes, // check/convert values of
		// attributes
		allowedClasses = currentRules.classes, i = 0, classes = [], newClasses = [], newUniqueClasses = [], oldClasses = [], classesLength, newClassesLength, currentClass, newClass, attributeName, newAttributeValue, method;

		if (setAttributes) {
			attributes = wysihtml5.lang.object(setAttributes).clone();
		}

		if (checkAttributes) {
			for (attributeName in checkAttributes) {
				method = attributeCheckMethods[checkAttributes[attributeName]];
				if (!method) {
					continue;
				}
				newAttributeValue = method(_getAttribute(oldNode, attributeName));
				if (typeof (newAttributeValue) === "string") {
					attributes[attributeName] = newAttributeValue;
				}
			}
		}

		if (setClass) {
			classes.push(setClass);
		}

		if (addClass) {
			for (attributeName in addClass) {
				method = addClassMethods[addClass[attributeName]];
				if (!method) {
					continue;
				}
				newClass = method(_getAttribute(oldNode, attributeName));
				if (typeof (newClass) === "string") {
					classes.push(newClass);
				}
			}
		}

		// make sure that wysihtml5 temp class doesn't get stripped out
		allowedClasses["_wysihtml5-temp-placeholder"] = 1;

		// add old classes last
		oldClasses = oldNode.getAttribute("class");
		if (oldClasses) {
			classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
		}
		classesLength = classes.length;
		for (; i < classesLength; i++) {
			currentClass = classes[i];
			if (allowedClasses[currentClass]) {
				newClasses.push(currentClass);
			}
		}

		// remove duplicate entries and preserve class specificity
		newClassesLength = newClasses.length;
		while (newClassesLength--) {
			currentClass = newClasses[newClassesLength];
			if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {
				newUniqueClasses.unshift(currentClass);
			}
		}

		if (newUniqueClasses.length) {
			attributes["class"] = newUniqueClasses.join(" ");
		}

		// set attributes on newNode
		for (attributeName in attributes) {
			// Setting attributes can cause a js error in IE under certain
			// circumstances
			// eg. on a <img> under https when it's new attribute value is
			// non-https
			// TODO: Investigate this further and check for smarter handling
			try {
				newNode.setAttribute(attributeName, attributes[attributeName]);
			} catch (e) {
			}
		}

		// IE8 sometimes loses the width/height attributes when those are set
		// before the "src"
		// so we make sure to set them again
		if (attributes.src) {
			if (typeof (attributes.width) !== "undefined") {
				newNode.setAttribute("width", attributes.width);
			}
			if (typeof (attributes.height) !== "undefined") {
				newNode.setAttribute("height", attributes.height);
			}
		}
	}

	/**
	 * IE gives wrong results for hasAttribute/getAttribute, for example: var td =
	 * document.createElement("td"); td.getAttribute("rowspan"); // => "1" in IE
	 * 
	 * Therefore we have to check the element's outerHTML for the attribute
	 */
	var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser
			.supportsGetAttributeCorrectly();
	function _getAttribute(node, attributeName) {
		attributeName = attributeName.toLowerCase();
		var nodeName = node.nodeName;
		if (nodeName == "IMG" && attributeName == "src"
				&& _isLoadedImage(node) === true) {
			// Get 'src' attribute value via object property since this will
			// always contain the
			// full absolute url (http://...)
			// this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8
			// where images copied from the same host
			// will have relative paths, which the sanitizer strips out (see
			// attributeCheckMethods.url)
			return node.src;
		} else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
			// Don't trust getAttribute/hasAttribute in IE 6-8, instead check
			// the element's outerHTML
			var outerHTML = node.outerHTML.toLowerCase(),
			// TODO: This might not work for attributes without value: <input
			// disabled>
			hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1;

			return hasAttribute ? node.getAttribute(attributeName) : null;
		} else {
			return node.getAttribute(attributeName);
		}
	}

	/**
	 * Check whether the given node is a proper loaded image FIXME: Returns
	 * undefined when unknown (Chrome, Safari)
	 */
	function _isLoadedImage(node) {
		try {
			return node.complete && !node.mozMatchesSelector(":-moz-broken");
		} catch (e) {
			if (node.complete && node.readyState === "complete") {
				return true;
			}
		}
	}

	function _handleText(oldNode) {
		return oldNode.ownerDocument.createTextNode(oldNode.data);
	}

	// ------------ attribute checks ------------ \\
	var attributeCheckMethods = {
		url : (function() {
			var REG_EXP = /^https?:\/\//i;
			return function(attributeValue) {
				if (!attributeValue || !attributeValue.match(REG_EXP)) {
					return null;
				}
				return attributeValue.replace(REG_EXP, function(match) {
					return match.toLowerCase();
				});
			};
		})(),

		alt : (function() {
			var REG_EXP = /[^ a-z0-9_\-]/gi;
			return function(attributeValue) {
				if (!attributeValue) {
					return "";
				}
				return attributeValue.replace(REG_EXP, "");
			};
		})(),

		numbers : (function() {
			var REG_EXP = /\D/g;
			return function(attributeValue) {
				attributeValue = (attributeValue || "").replace(REG_EXP, "");
				return attributeValue || null;
			};
		})()
	};

	// ------------ class converter (converts an html attribute to a class name)
	// ------------ \\
	var addClassMethods = {
		align_img : (function() {
			var mapping = {
				left : "wysiwyg-float-left",
				right : "wysiwyg-float-right"
			};
			return function(attributeValue) {
				return mapping[String(attributeValue).toLowerCase()];
			};
		})(),

		align_text : (function() {
			var mapping = {
				left : "wysiwyg-text-align-left",
				right : "wysiwyg-text-align-right",
				center : "wysiwyg-text-align-center",
				justify : "wysiwyg-text-align-justify"
			};
			return function(attributeValue) {
				return mapping[String(attributeValue).toLowerCase()];
			};
		})(),

		clear_br : (function() {
			var mapping = {
				left : "wysiwyg-clear-left",
				right : "wysiwyg-clear-right",
				both : "wysiwyg-clear-both",
				all : "wysiwyg-clear-both"
			};
			return function(attributeValue) {
				return mapping[String(attributeValue).toLowerCase()];
			};
		})(),

		size_font : (function() {
			var mapping = {
				"1" : "wysiwyg-font-size-xx-small",
				"2" : "wysiwyg-font-size-small",
				"3" : "wysiwyg-font-size-medium",
				"4" : "wysiwyg-font-size-large",
				"5" : "wysiwyg-font-size-x-large",
				"6" : "wysiwyg-font-size-xx-large",
				"7" : "wysiwyg-font-size-xx-large",
				"-" : "wysiwyg-font-size-smaller",
				"+" : "wysiwyg-font-size-larger"
			};
			return function(attributeValue) {
				return mapping[String(attributeValue).charAt(0)];
			};
		})()
	};

	return parse;
})();
/**
 * Checks for empty text node childs and removes them
 * 
 * @param {Element}
 *            node The element in which to cleanup
 * @example wysihtml5.dom.removeEmptyTextNodes(element);
 */
wysihtml5.dom.removeEmptyTextNodes = function(node) {
	var childNode, childNodes = wysihtml5.lang.array(node.childNodes).get(), childNodesLength = childNodes.length, i = 0;
	for (; i < childNodesLength; i++) {
		childNode = childNodes[i];
		if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
			childNode.parentNode.removeChild(childNode);
		}
	}
};
/**
 * Renames an element (eg. a <div> to a
 * <p>) and keeps its childs
 * 
 * @param {Element}
 *            element The list element which should be renamed
 * @param {Element}
 *            newNodeName The desired tag name
 * 
 * @example <!-- Assume the following dom: -->
 *          <ul id="list">
 *          <li>eminem</li>
 *          <li>dr. dre</li>
 *          <li>50 Cent</li>
 *          </ul>
 * 
 * <script> wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
 * </script>
 * 
 * <!-- Will result in: -->
 * <ol>
 * <li>eminem</li>
 * <li>dr. dre</li>
 * <li>50 Cent</li>
 * </ol>
 */
wysihtml5.dom.renameElement = function(element, newNodeName) {
	var newElement = element.ownerDocument.createElement(newNodeName), firstChild;
	while (firstChild = element.firstChild) {
		newElement.appendChild(firstChild);
	}
	wysihtml5.dom.copyAttributes([ "align", "className" ]).from(element).to(
			newElement);
	element.parentNode.replaceChild(newElement, element);
	return newElement;
};
/**
 * Takes an element, removes it and replaces it with it's childs
 * 
 * @param {Object}
 *            node The node which to replace with it's child nodes
 * @example <div id="foo"> <span>hello</span> </div> <script> // Remove #foo
 *          and replace with it's children
 *          wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
 *          </script>
 */
wysihtml5.dom.replaceWithChildNodes = function(node) {
	if (!node.parentNode) {
		return;
	}

	if (!node.firstChild) {
		node.parentNode.removeChild(node);
		return;
	}

	var fragment = node.ownerDocument.createDocumentFragment();
	while (node.firstChild) {
		fragment.appendChild(node.firstChild);
	}
	node.parentNode.replaceChild(fragment, node);
	node = fragment = null;
};
/**
 * Unwraps an unordered/ordered list
 * 
 * @param {Element}
 *            element The list element which should be unwrapped
 * 
 * @example <!-- Assume the following dom: -->
 *          <ul id="list">
 *          <li>eminem</li>
 *          <li>dr. dre</li>
 *          <li>50 Cent</li>
 *          </ul>
 * 
 * <script> wysihtml5.dom.resolveList(document.getElementById("list"));
 * </script>
 * 
 * <!-- Will result in: --> eminem<br>
 * dr. dre<br>
 * 50 Cent<br>
 */
(function(dom) {
	function _isBlockElement(node) {
		return dom.getStyle("display").from(node) === "block";
	}

	function _isLineBreak(node) {
		return node.nodeName === "BR";
	}

	function _appendLineBreak(element) {
		var lineBreak = element.ownerDocument.createElement("br");
		element.appendChild(lineBreak);
	}

	function resolveList(list) {
		if (list.nodeName !== "MENU" && list.nodeName !== "UL"
				&& list.nodeName !== "OL") {
			return;
		}

		var doc = list.ownerDocument, fragment = doc.createDocumentFragment(), previousSibling = list.previousElementSibling
				|| list.previousSibling, firstChild, lastChild, isLastChild, shouldAppendLineBreak, listItem;

		if (previousSibling && !_isBlockElement(previousSibling)) {
			_appendLineBreak(fragment);
		}

		while (listItem = list.firstChild) {
			lastChild = listItem.lastChild;
			while (firstChild = listItem.firstChild) {
				isLastChild = firstChild === lastChild;
				// This needs to be done before appending it to the fragment, as
				// it otherwise will loose style information
				shouldAppendLineBreak = isLastChild
						&& !_isBlockElement(firstChild)
						&& !_isLineBreak(firstChild);
				fragment.appendChild(firstChild);
				if (shouldAppendLineBreak) {
					_appendLineBreak(fragment);
				}
			}

			listItem.parentNode.removeChild(listItem);
		}
		list.parentNode.replaceChild(fragment, list);
	}

	dom.resolveList = resolveList;
})(wysihtml5.dom);
/**
 * Sandbox for executing javascript, parsing css styles and doing dom operations
 * in a secure way
 * 
 * Browser Compatibility: - Secure in MSIE 6+, but only when the user hasn't
 * made changes to his security level "restricted" - Partially secure in other
 * browsers (Firefox, Opera, Safari, Chrome, ...)
 * 
 * Please note that this class can't benefit from the HTML5 sandbox attribute
 * for the following reasons: - sandboxing doesn't work correctly with inlined
 * content (src="javascript:'<html>...</html>'") - sandboxing of physical
 * documents causes that the dom isn't accessible anymore from the outside
 * (iframe.contentWindow, ...) - setting the "allow-same-origin" flag would fix
 * that, but then still javascript and dom events refuse to fire - therefore the
 * "allow-scripts" flag is needed, which then would deactivate any security, as
 * the js executed inside the iframe can do anything as if the sandbox attribute
 * wasn't set
 * 
 * @param {Function}
 *            [readyCallback] Method that gets invoked when the sandbox is ready
 * @param {Object}
 *            [config] Optional parameters
 * 
 * @example new wysihtml5.dom.Sandbox(function(sandbox) {
 *          sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif
 *          onerror="alert(document.cookie)">'; });
 */
(function(wysihtml5) {
	var /**
		 * Default configuration
		 */
	doc = document,
	/**
	 * Properties to unset/protect on the window object
	 */
	windowProperties = [ "parent", "top", "opener", "frameElement", "frames",
			"localStorage", "globalStorage", "sessionStorage", "indexedDB" ],
	/**
	 * Properties on the window object which are set to an empty function
	 */
	windowProperties2 = [ "open", "close", "openDialog", "showModalDialog",
			"alert", "confirm", "prompt", "openDatabase", "postMessage",
			"XMLHttpRequest", "XDomainRequest" ],
	/**
	 * Properties to unset/protect on the document object
	 */
	documentProperties = [ "referrer", "write", "open", "close" ];

	wysihtml5.dom.Sandbox = Base
			.extend(
			/** @scope wysihtml5.dom.Sandbox.prototype */
			{

				constructor : function(readyCallback, config) {
					this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
					this.config = wysihtml5.lang.object({}).merge(config).get();
					this.iframe = this._createIframe();
				},

				insertInto : function(element) {
					if (typeof (element) === "string") {
						element = doc.getElementById(element);
					}

					element.appendChild(this.iframe);
				},

				getIframe : function() {
					return this.iframe;
				},

				getWindow : function() {
					this._readyError();
				},

				getDocument : function() {
					this._readyError();
				},

				destroy : function() {
					var iframe = this.getIframe();
					iframe.parentNode.removeChild(iframe);
				},

				_readyError : function() {
					throw new Error(
							"wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
				},

				/**
				 * Creates the sandbox iframe
				 * 
				 * Some important notes: - We can't use HTML5 sandbox for now:
				 * setting it causes that the iframe's dom can't be accessed
				 * from the outside Therefore we need to set the
				 * "allow-same-origin" flag which enables accessing the iframe's
				 * dom But then there's another problem, DOM events (focus,
				 * blur, change, keypress, ...) aren't fired. In order to make
				 * this happen we need to set the "allow-scripts" flag. A
				 * combination of allow-scripts and allow-same-origin is almost
				 * the same as setting no sandbox attribute at all. - Chrome &
				 * Safari, doesn't seem to support sandboxing correctly when the
				 * iframe's html is inlined (no physical document) - IE needs to
				 * have the security="restricted" attribute set before the
				 * iframe is inserted into the dom tree - Believe it or not but
				 * in IE "security" in document.createElement("iframe") is
				 * false, even though it supports it - When an iframe has
				 * security="restricted", in IE eval() & execScript() don't work
				 * anymore - IE doesn't fire the onload event when the content
				 * is inlined in the src attribute, therefore we rely on the
				 * onreadystatechange event
				 */
				_createIframe : function() {
					var that = this, iframe = doc.createElement("iframe");
					iframe.className = "wysihtml5-sandbox";
					wysihtml5.dom.setAttributes({
						"security" : "restricted",
						"allowtransparency" : "true",
						"frameborder" : 0,
						"width" : 0,
						"height" : 0,
						"marginwidth" : 0,
						"marginheight" : 0
					}).on(iframe);

					// Setting the src like this prevents ssl warnings in IE6
					if (wysihtml5.browser
							.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
						iframe.src = "javascript:'<html></html>'";
					}

					iframe.onload = function() {
						iframe.onreadystatechange = iframe.onload = null;
						that._onLoadIframe(iframe);
					};

					iframe.onreadystatechange = function() {
						if (/loaded|complete/.test(iframe.readyState)) {
							iframe.onreadystatechange = iframe.onload = null;
							that._onLoadIframe(iframe);
						}
					};

					return iframe;
				},

				/**
				 * Callback for when the iframe has finished loading
				 */
				_onLoadIframe : function(iframe) {
					// don't resume when the iframe got unloaded (eg. by
					// removing it from the dom)
					if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
						return;
					}

					var that = this, iframeWindow = iframe.contentWindow, iframeDocument = iframe.contentWindow.document, charset = doc.characterSet
							|| doc.charset || "utf-8", sandboxHtml = this
							._getHtml({
								charset : charset,
								stylesheets : this.config.stylesheets
							});

					// Create the basic dom tree including proper DOCTYPE and
					// charset
					iframeDocument.open("text/html", "replace");
					iframeDocument.write(sandboxHtml);
					iframeDocument.close();

					this.getWindow = function() {
						return iframe.contentWindow;
					};
					this.getDocument = function() {
						return iframe.contentWindow.document;
					};

					// Catch js errors and pass them to the parent's onerror
					// event
					// addEventListener("error") doesn't work properly in some
					// browsers
					// TODO: apparently this doesn't work in IE9!
					iframeWindow.onerror = function(errorMessage, fileName,
							lineNumber) {
						throw new Error("wysihtml5.Sandbox: " + errorMessage,
								fileName, lineNumber);
					};

					if (!wysihtml5.browser.supportsSandboxedIframes()) {
						// Unset a bunch of sensitive variables
						// Please note: This isn't hack safe!
						// It more or less just takes care of basic attacks and
						// prevents accidental theft of sensitive information
						// IE is secure though, which is the most important
						// thing, since IE is the only browser, who
						// takes over scripts & styles into contentEditable
						// elements when copied from external websites
						// or applications (Microsoft Word, ...)
						var i, length;
						for (i = 0, length = windowProperties.length; i < length; i++) {
							this._unset(iframeWindow, windowProperties[i]);
						}
						for (i = 0, length = windowProperties2.length; i < length; i++) {
							this._unset(iframeWindow, windowProperties2[i],
									wysihtml5.EMPTY_FUNCTION);
						}
						for (i = 0, length = documentProperties.length; i < length; i++) {
							this._unset(iframeDocument, documentProperties[i]);
						}
						// This doesn't work in Safari 5
						// See
						// http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
						this._unset(iframeDocument, "cookie", "", true);
					}

					this.loaded = true;

					// Trigger the callback
					setTimeout(function() {
						that.callback(that);
					}, 0);
				},

				_getHtml : function(templateVars) {
					var stylesheets = templateVars.stylesheets, html = "", i = 0, length;
					stylesheets = typeof (stylesheets) === "string" ? [ stylesheets ]
							: stylesheets;
					if (stylesheets) {
						length = stylesheets.length;
						for (; i < length; i++) {
							html += '<link rel="stylesheet" href="'
									+ stylesheets[i] + '">';
						}
					}
					templateVars.stylesheets = html;

					return wysihtml5.lang
							.string(
									'<!DOCTYPE html><html><head>'
											+ '<meta charset="#{charset}">#{stylesheets}</head>'
											+ '<body></body></html>')
							.interpolate(templateVars);
				},

				/**
				 * Method to unset/override existing variables
				 * 
				 * @example // Make cookie unreadable and unwritable
				 *          this._unset(document, "cookie", "", true);
				 */
				_unset : function(object, property, value, setter) {
					try {
						object[property] = value;
					} catch (e) {
					}

					try {
						object.__defineGetter__(property, function() {
							return value;
						});
					} catch (e) {
					}
					if (setter) {
						try {
							object.__defineSetter__(property, function() {
							});
						} catch (e) {
						}
					}

					if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
						try {
							var config = {
								get : function() {
									return value;
								}
							};
							if (setter) {
								config.set = function() {
								};
							}
							Object.defineProperty(object, property, config);
						} catch (e) {
						}
					}
				}
			});
})(wysihtml5);
(function() {
	var mapping = {
		"className" : "class"
	};
	wysihtml5.dom.setAttributes = function(attributes) {
		return {
			on : function(element) {
				for ( var i in attributes) {
					element.setAttribute(mapping[i] || i, attributes[i]);
				}
			}
		}
	};
})();
wysihtml5.dom.setStyles = function(styles) {
	return {
		on : function(element) {
			var style = element.style;
			if (typeof (styles) === "string") {
				style.cssText += ";" + styles;
				return;
			}
			for ( var i in styles) {
				if (i === "float") {
					style.cssFloat = styles[i];
					style.styleFloat = styles[i];
				} else {
					style[i] = styles[i];
				}
			}
		}
	};
};
/**
 * Simulate HTML5 placeholder attribute
 * 
 * Needed since - div[contentEditable] elements don't support it - older
 * browsers (such as IE8 and Firefox 3.6) don't support it at all
 * 
 * @param {Object}
 *            parent Instance of main wysihtml5.Editor class
 * @param {Element}
 *            view Instance of wysihtml5.views.* class
 * @param {String}
 *            placeholderText
 * 
 * @example wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
 */
(function(dom) {
	dom.simulatePlaceholder = function(editor, view, placeholderText) {
		var CLASS_NAME = "placeholder", unset = function() {
			if (view.hasPlaceholderSet()) {
				view.clear();
			}
			dom.removeClass(view.element, CLASS_NAME);
		}, set = function() {
			if (view.isEmpty()) {
				view.setValue(placeholderText);
				dom.addClass(view.element, CLASS_NAME);
			}
		};

		editor.observe("set_placeholder", set).observe("unset_placeholder",
				unset).observe("focus:composer", unset).observe(
				"paste:composer", unset).observe("blur:composer", set);

		set();
	};
})(wysihtml5.dom);
(function(dom) {
	var documentElement = document.documentElement;
	if ("textContent" in documentElement) {
		dom.setTextContent = function(element, text) {
			element.textContent = text;
		};

		dom.getTextContent = function(element) {
			return element.textContent;
		};
	} else if ("innerText" in documentElement) {
		dom.setTextContent = function(element, text) {
			element.innerText = text;
		};

		dom.getTextContent = function(element) {
			return element.innerText;
		};
	} else {
		dom.setTextContent = function(element, text) {
			element.nodeValue = text;
		};

		dom.getTextContent = function(element) {
			return element.nodeValue;
		};
	}
})(wysihtml5.dom);

/**
 * Fix most common html formatting misbehaviors of browsers implementation when
 * inserting content via copy & paste contentEditable
 * 
 * @author Christopher Blum
 */
wysihtml5.quirks.cleanPastedHTML = (function() {
	// TODO: We probably need more rules here
	var defaultRules = {
		// When pasting underlined links <a> into a contentEditable, IE thinks,
		// it has to insert <u> to keep the styling
		"a u" : wysihtml5.dom.replaceWithChildNodes
	};

	function cleanPastedHTML(elementOrHtml, rules, context) {
		rules = rules || defaultRules;
		context = context || elementOrHtml.ownerDocument || document;

		var element, isString = typeof (elementOrHtml) === "string", method, matches, matchesLength, i, j = 0;
		if (isString) {
			element = wysihtml5.dom.getAsDom(elementOrHtml, context);
		} else {
			element = elementOrHtml;
		}

		for (i in rules) {
			matches = element.querySelectorAll(i);
			method = rules[i];
			matchesLength = matches.length;
			for (; j < matchesLength; j++) {
				method(matches[j]);
			}
		}

		matches = elementOrHtml = rules = null;

		return isString ? element.innerHTML : element;
	}

	return cleanPastedHTML;
})();
/**
 * IE and Opera leave an empty paragraph in the contentEditable element after
 * clearing it
 * 
 * @param {Object}
 *            contentEditableElement The contentEditable element to observe for
 *            clearing events
 * @exaple wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
 */
(function(wysihtml5) {
	var dom = wysihtml5.dom;

	wysihtml5.quirks.ensureProperClearing = (function() {
		var clearIfNecessary = function(event) {
			var element = this;
			setTimeout(function() {
				var innerHTML = element.innerHTML.toLowerCase();
				if (innerHTML == "<p>&nbsp;</p>"
						|| innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
					element.innerHTML = "";
				}
			}, 0);
		};

		return function(composer) {
			dom.observe(composer.element, [ "cut", "keydown" ],
					clearIfNecessary);
		};
	})();

	/**
	 * In Opera when the caret is in the first and only item of a list (
	 * <ul>
	 * <li>|</li>
	 * </ul>) and the list is the first child of the contentEditable element,
	 * it's impossible to delete the list by hitting backspace
	 * 
	 * @param {Object}
	 *            contentEditableElement The contentEditable element to observe
	 *            for clearing events
	 * @exaple wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
	 */
	wysihtml5.quirks.ensureProperClearingOfLists = (function() {
		var ELEMENTS_THAT_CONTAIN_LI = [ "OL", "UL", "MENU" ];

		var clearIfNecessary = function(element, contentEditableElement) {
			if (!contentEditableElement.firstChild
					|| !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI)
							.contains(
									contentEditableElement.firstChild.nodeName)) {
				return;
			}

			var list = dom.getParentElement(element, {
				nodeName : ELEMENTS_THAT_CONTAIN_LI
			});
			if (!list) {
				return;
			}

			var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;
			if (!listIsFirstChildOfContentEditable) {
				return;
			}

			var hasOnlyOneListItem = list.childNodes.length <= 1;
			if (!hasOnlyOneListItem) {
				return;
			}

			var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === ""
					: true;
			if (!onlyListItemIsEmpty) {
				return;
			}

			list.parentNode.removeChild(list);
		};

		return function(composer) {
			dom.observe(composer.element, "keydown", function(event) {
				if (event.keyCode !== wysihtml5.BACKSPACE_KEY) {
					return;
				}

				var element = composer.selection.getSelectedNode();
				clearIfNecessary(element, composer.element);
			});
		};
	})();

})(wysihtml5);
// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
//
// In Firefox this:
// var d = document.createElement("div");
// d.innerHTML ='<a href="~"></a>';
// d.innerHTML;
// will result in:
// <a href="%7E"></a>
// which is wrong
(function(wysihtml5) {
	var TILDE_ESCAPED = "%7E";
	wysihtml5.quirks.getCorrectInnerHTML = function(element) {
		var innerHTML = element.innerHTML;
		if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
			return innerHTML;
		}

		var elementsWithTilde = element
				.querySelectorAll("[href*='~'], [src*='~']"), url, urlToSearch, length, i;
		for (i = 0, length = elementsWithTilde.length; i < length; i++) {
			url = elementsWithTilde[i].href || elementsWithTilde[i].src;
			urlToSearch = wysihtml5.lang.string(url).replace("~").by(
					TILDE_ESCAPED);
			innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch)
					.by(url);
		}
		return innerHTML;
	};
})(wysihtml5);
/**
 * Some browsers don't insert line breaks when hitting return in a
 * contentEditable element - Opera & IE insert new
 * <p>
 * on return - Chrome & Safari insert new <div> on return - Firefox inserts <br>
 * on return (yippie!)
 * 
 * @param {Element}
 *            element
 * 
 * @example wysihtml5.quirks.insertLineBreakOnReturn(element);
 */
(function(wysihtml5) {
	var dom = wysihtml5.dom, USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = [
			"LI", "P", "H1", "H2", "H3", "H4", "H5", "H6" ], LIST_TAGS = [
			"UL", "OL", "MENU" ];

	wysihtml5.quirks.insertLineBreakOnReturn = function(composer) {
		function unwrap(selectedNode) {
			var parentElement = dom.getParentElement(selectedNode, {
				nodeName : [ "P", "DIV" ]
			}, 2);
			if (!parentElement) {
				return;
			}

			var invisibleSpace = document
					.createTextNode(wysihtml5.INVISIBLE_SPACE);
			dom.insert(invisibleSpace).before(parentElement);
			dom.replaceWithChildNodes(parentElement);
			composer.selection.selectNode(invisibleSpace);
		}

		function keyDown(event) {
			var keyCode = event.keyCode;
			if (event.shiftKey
					|| (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {
				return;
			}

			var element = event.target, selectedNode = composer.selection
					.getSelectedNode(), blockElement = dom.getParentElement(
					selectedNode, {
						nodeName : USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS
					}, 4);
			if (blockElement) {
				// Some browsers create <p> elements after leaving a list
				// check after keydown of backspace and return whether a <p> got
				// inserted and unwrap it
				if (blockElement.nodeName === "LI"
						&& (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {
					setTimeout(
							function() {
								var selectedNode = composer.selection
										.getSelectedNode(), list, div;
								if (!selectedNode) {
									return;
								}

								list = dom.getParentElement(selectedNode, {
									nodeName : LIST_TAGS
								}, 2);

								if (list) {
									return;
								}

								unwrap(selectedNode);
							}, 0);
				} else if (blockElement.nodeName.match(/H[1-6]/)
						&& keyCode === wysihtml5.ENTER_KEY) {
					setTimeout(function() {
						unwrap(composer.selection.getSelectedNode());
					}, 0);
				}
				return;
			}

			if (keyCode === wysihtml5.ENTER_KEY
					&& !wysihtml5.browser.insertsLineBreaksOnReturn()) {
				composer.commands.exec("insertLineBreak");
				event.preventDefault();
			}
		}

		// keypress doesn't fire when you hit backspace
		dom.observe(composer.element.ownerDocument, "keydown", keyDown);
	};
})(wysihtml5);
/**
 * Force rerendering of a given element Needed to fix display misbehaviors of IE
 * 
 * @param {Element}
 *            element The element object which needs to be rerendered
 * @example wysihtml5.quirks.redraw(document.body);
 */
(function(wysihtml5) {
	var CLASS_NAME = "wysihtml5-quirks-redraw";

	wysihtml5.quirks.redraw = function(element) {
		wysihtml5.dom.addClass(element, CLASS_NAME);
		wysihtml5.dom.removeClass(element, CLASS_NAME);

		// Following hack is needed for firefox to make sure that image resize
		// handles are properly removed
		try {
			var doc = element.ownerDocument;
			doc.execCommand("italic", false, null);
			doc.execCommand("italic", false, null);
		} catch (e) {
		}
	};
})(wysihtml5);
/**
 * Selection API
 * 
 * @example var selection = new wysihtml5.Selection(editor);
 */
(function(wysihtml5) {
	var dom = wysihtml5.dom;

	function _getCumulativeOffsetTop(element) {
		var top = 0;
		if (element.parentNode) {
			do {
				top += element.offsetTop || 0;
				element = element.offsetParent;
			} while (element);
		}
		return top;
	}

	wysihtml5.Selection = Base
			.extend(
			/** @scope wysihtml5.Selection.prototype */
			{
				constructor : function(editor) {
					// Make sure that our external range library is initialized
					window.rangy.init();

					this.editor = editor;
					this.composer = editor.composer;
					this.doc = this.composer.doc;
				},

				/**
				 * Get the current selection as a bookmark to be able to later
				 * restore it
				 * 
				 * @return {Object} An object that represents the current
				 *         selection
				 */
				getBookmark : function() {
					var range = this.getRange();
					return range && range.cloneRange();
				},

				/**
				 * Restore a selection retrieved via
				 * wysihtml5.Selection.prototype.getBookmark
				 * 
				 * @param {Object}
				 *            bookmark An object that represents the current
				 *            selection
				 */
				setBookmark : function(bookmark) {
					if (!bookmark) {
						return;
					}

					this.setSelection(bookmark);
				},

				/**
				 * Set the caret in front of the given node
				 * 
				 * @param {Object}
				 *            node The element or text node where to position
				 *            the caret in front of
				 * @example selection.setBefore(myElement);
				 */
				setBefore : function(node) {
					var range = rangy.createRange(this.doc);
					range.setStartBefore(node);
					range.setEndBefore(node);
					return this.setSelection(range);
				},

				/**
				 * Set the caret after the given node
				 * 
				 * @param {Object}
				 *            node The element or text node where to position
				 *            the caret in front of
				 * @example selection.setBefore(myElement);
				 */
				setAfter : function(node) {
					var range = rangy.createRange(this.doc);
					range.setStartAfter(node);
					range.setEndAfter(node);
					return this.setSelection(range);
				},

				/**
				 * Ability to select/mark nodes
				 * 
				 * @param {Element}
				 *            node The node/element to select
				 * @example selection.selectNode(document.getElementById("my-image"));
				 */
				selectNode : function(node) {
					var range = rangy.createRange(this.doc), isElement = node.nodeType === wysihtml5.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML
							: (node.nodeName !== "IMG"), content = isElement ? node.innerHTML
							: node.data, isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), displayStyle = dom
							.getStyle("display").from(node), isBlockElement = (displayStyle === "block" || displayStyle === "list-item");

					if (isEmpty && isElement && canHaveHTML) {
						// Make sure that caret is visible in node by inserting
						// a zero width no breaking space
						try {
							node.innerHTML = wysihtml5.INVISIBLE_SPACE;
						} catch (e) {
						}
					}

					if (canHaveHTML) {
						range.selectNodeContents(node);
					} else {
						range.selectNode(node);
					}

					if (canHaveHTML && isEmpty && isElement) {
						range.collapse(isBlockElement);
					} else if (canHaveHTML && isEmpty) {
						range.setStartAfter(node);
						range.setEndAfter(node);
					}

					this.setSelection(range);
				},

				/**
				 * Get the node which contains the selection
				 * 
				 * @param {Boolean}
				 *            [controlRange] (only IE) Whether it should return
				 *            the selected ControlRange element when the
				 *            selection type is a "ControlRange"
				 * @return {Object} The node that contains the caret
				 * @example var nodeThatContainsCaret =
				 *          selection.getSelectedNode();
				 */
				getSelectedNode : function(controlRange) {
					var selection, range;

					if (controlRange && this.doc.selection
							&& this.doc.selection.type === "Control") {
						range = this.doc.selection.createRange();
						if (range && range.length) {
							return range.item(0);
						}
					}

					selection = this.getSelection(this.doc);
					if (selection.focusNode === selection.anchorNode) {
						return selection.focusNode;
					} else {
						range = this.getRange(this.doc);
						return range ? range.commonAncestorContainer
								: this.doc.body;
					}
				},

				executeAndRestore : function(method, restoreScrollPosition) {
					var body = this.doc.body, oldScrollTop = restoreScrollPosition
							&& body.scrollTop, oldScrollLeft = restoreScrollPosition
							&& body.scrollLeft, className = "_wysihtml5-temp-placeholder", placeholderHTML = '<span class="'
							+ className
							+ '">'
							+ wysihtml5.INVISIBLE_SPACE
							+ '</span>', range = this.getRange(this.doc), newRange;

					// Nothing selected, execute and say goodbye
					if (!range) {
						method(body, body);
						return;
					}

					var node = range.createContextualFragment(placeholderHTML);
					range.insertNode(node);

					// Make sure that a potential error doesn't cause our
					// placeholder element to be left as a placeholder
					try {
						method(range.startContainer, range.endContainer);
					} catch (e3) {
						setTimeout(function() {
							throw e3;
						}, 0);
					}

					caretPlaceholder = this.doc.querySelector("." + className);
					if (caretPlaceholder) {
						newRange = rangy.createRange(this.doc);
						newRange.selectNode(caretPlaceholder);
						newRange.deleteContents();
						this.setSelection(newRange);
					} else {
						// fallback for when all hell breaks loose
						body.focus();
					}

					if (restoreScrollPosition) {
						body.scrollTop = oldScrollTop;
						body.scrollLeft = oldScrollLeft;
					}

					// Remove it again, just to make sure that the placeholder
					// is definitely out of the dom tree
					try {
						caretPlaceholder.parentNode
								.removeChild(caretPlaceholder);
					} catch (e4) {
					}
				},

				/**
				 * Different approach of preserving the selection (doesn't
				 * modify the dom) Takes all text nodes in the selection and
				 * saves the selection position in the first and last one
				 */
				executeAndRestoreSimple : function(method) {
					var range = this.getRange(), body = this.doc.body, newRange, firstNode, lastNode, textNodes, rangeBackup;

					// Nothing selected, execute and say goodbye
					if (!range) {
						method(body, body);
						return;
					}

					textNodes = range.getNodes([ 3 ]);
					firstNode = textNodes[0] || range.startContainer;
					lastNode = textNodes[textNodes.length - 1]
							|| range.endContainer;

					rangeBackup = {
						collapsed : range.collapsed,
						startContainer : firstNode,
						startOffset : firstNode === range.startContainer ? range.startOffset
								: 0,
						endContainer : lastNode,
						endOffset : lastNode === range.endContainer ? range.endOffset
								: lastNode.length
					};

					try {
						method(range.startContainer, range.endContainer);
					} catch (e) {
						setTimeout(function() {
							throw e;
						}, 0);
					}

					newRange = rangy.createRange(this.doc);
					try {
						newRange.setStart(rangeBackup.startContainer,
								rangeBackup.startOffset);
					} catch (e1) {
					}
					try {
						newRange.setEnd(rangeBackup.endContainer,
								rangeBackup.endOffset);
					} catch (e2) {
					}
					try {
						this.setSelection(newRange);
					} catch (e3) {
					}
				},

				/**
				 * Insert html at the caret position and move the cursor after
				 * the inserted html
				 * 
				 * @param {String}
				 *            html HTML string to insert
				 * @example selection.insertHTML("
				 *          <p>
				 *          foobar
				 *          </p>
				 *          ");
				 */
				insertHTML : function(html) {
					var range = rangy.createRange(this.doc), node = range
							.createContextualFragment(html), lastChild = node.lastChild;
					this.insertNode(node);
					if (lastChild) {
						this.setAfter(lastChild);
					}
				},

				/**
				 * Insert a node at the caret position and move the cursor
				 * behind it
				 * 
				 * @param {Object}
				 *            node HTML string to insert
				 * @example selection.insertNode(document.createTextNode("foobar"));
				 */
				insertNode : function(node) {
					var range = this.getRange();
					if (range) {
						range.insertNode(node);
					}
				},

				/**
				 * Wraps current selection with the given node
				 * 
				 * @param {Object}
				 *            node The node to surround the selected elements
				 *            with
				 */
				surround : function(node) {
					var range = this.getRange();
					if (!range) {
						return;
					}

					try {
						// This only works when the range boundaries are not
						// overlapping other elements
						range.surroundContents(node);
						this.selectNode(node);
					} catch (e) {
						// fallback
						node.appendChild(range.extractContents());
						range.insertNode(node);
					}
				},

				/**
				 * Scroll the current caret position into the view FIXME: This
				 * is a bit hacky, there might be a smarter way of doing this
				 * 
				 * @example selection.scrollIntoView();
				 */
				scrollIntoView : function() {
					var doc = this.doc, hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement
							|| (function() {
								var element = doc.createElement("span");
								// The element needs content in order to be able
								// to calculate it's position properly
								element.innerHTML = wysihtml5.INVISIBLE_SPACE;
								return element;
							})(), offsetTop;

					if (hasScrollBars) {
						this.insertNode(tempElement);
						offsetTop = _getCumulativeOffsetTop(tempElement);
						tempElement.parentNode.removeChild(tempElement);
						if (offsetTop > doc.body.scrollTop) {
							doc.body.scrollTop = offsetTop;
						}
					}
				},

				/**
				 * Select line where the caret is in
				 */
				selectLine : function() {
					if (wysihtml5.browser.supportsSelectionModify()) {
						this._selectLine_W3C();
					} else if (this.doc.selection) {
						this._selectLine_MSIE();
					}
				},

				/**
				 * See https://developer.mozilla.org/en/DOM/Selection/modify
				 */
				_selectLine_W3C : function() {
					var win = this.doc.defaultView, selection = win
							.getSelection();
					selection.modify("extend", "left", "lineboundary");
					selection.modify("extend", "right", "lineboundary");
				},

				_selectLine_MSIE : function() {
					var range = this.doc.selection.createRange(), rangeTop = range.boundingTop, rangeHeight = range.boundingHeight, scrollWidth = this.doc.body.scrollWidth, rangeBottom, rangeEnd, measureNode, i, j;

					if (!range.moveToPoint) {
						return;
					}

					if (rangeTop === 0) {
						// Don't know why, but when the selection ends at the
						// end of a line
						// range.boundingTop is 0
						measureNode = this.doc.createElement("span");
						this.insertNode(measureNode);
						rangeTop = measureNode.offsetTop;
						measureNode.parentNode.removeChild(measureNode);
					}

					rangeTop += 1;

					for (i = -10; i < scrollWidth; i += 2) {
						try {
							range.moveToPoint(i, rangeTop);
							break;
						} catch (e1) {
						}
					}

					// Investigate the following in order to handle multi line
					// selections
					// rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1)
					// : 0);
					rangeBottom = rangeTop;
					rangeEnd = this.doc.selection.createRange();
					for (j = scrollWidth; j >= 0; j--) {
						try {
							rangeEnd.moveToPoint(j, rangeBottom);
							break;
						} catch (e2) {
						}
					}

					range.setEndPoint("EndToEnd", rangeEnd);
					range.select();
				},

				getText : function() {
					var selection = this.getSelection();
					return selection ? selection.toString() : "";
				},

				getNodes : function(nodeType, filter) {
					var range = this.getRange();
					if (range) {
						return range.getNodes([ nodeType ], filter);
					} else {
						return [];
					}
				},

				getRange : function() {
					var selection = this.getSelection();
					return selection && selection.rangeCount
							&& selection.getRangeAt(0);
				},

				getSelection : function() {
					return rangy.getSelection(this.doc.defaultView
							|| this.doc.parentWindow);
				},

				setSelection : function(range) {
					var win = this.doc.defaultView || this.doc.parentWindow, selection = rangy
							.getSelection(win);
					return selection.setSingleRange(range);
				}
			});

})(wysihtml5);
/**
 * Inspired by the rangy CSS Applier module written by Tim Down and licensed
 * under the MIT license. http://code.google.com/p/rangy/
 * 
 * changed in order to be able ... - to use custom tags - to detect and replace
 * similar css classes via reg exp
 */
(function(wysihtml5, rangy) {
	var defaultTagName = "span";

	var REG_EXP_WHITE_SPACE = /\s+/g;

	function hasClass(el, cssClass, regExp) {
		if (!el.className) {
			return false;
		}

		var matchingClassNames = el.className.match(regExp) || [];
		return matchingClassNames[matchingClassNames.length - 1] === cssClass;
	}

	function addClass(el, cssClass, regExp) {
		if (el.className) {
			removeClass(el, regExp);
			el.className += " " + cssClass;
		} else {
			el.className = cssClass;
		}
	}

	function removeClass(el, regExp) {
		if (el.className) {
			el.className = el.className.replace(regExp, "");
		}
	}

	function hasSameClasses(el1, el2) {
		return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className
				.replace(REG_EXP_WHITE_SPACE, " ");
	}

	function replaceWithOwnChildren(el) {
		var parent = el.parentNode;
		while (el.firstChild) {
			parent.insertBefore(el.firstChild, el);
		}
		parent.removeChild(el);
	}

	function elementsHaveSameNonClassAttributes(el1, el2) {
		if (el1.attributes.length != el2.attributes.length) {
			return false;
		}
		for ( var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
			attr1 = el1.attributes[i];
			name = attr1.name;
			if (name != "class") {
				attr2 = el2.attributes.getNamedItem(name);
				if (attr1.specified != attr2.specified) {
					return false;
				}
				if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
					return false;
				}
			}
		}
		return true;
	}

	function isSplitPoint(node, offset) {
		if (rangy.dom.isCharacterDataNode(node)) {
			if (offset == 0) {
				return !!node.previousSibling;
			} else if (offset == node.length) {
				return !!node.nextSibling;
			} else {
				return true;
			}
		}

		return offset > 0 && offset < node.childNodes.length;
	}

	function splitNodeAt(node, descendantNode, descendantOffset) {
		var newNode;
		if (rangy.dom.isCharacterDataNode(descendantNode)) {
			if (descendantOffset == 0) {
				descendantOffset = rangy.dom.getNodeIndex(descendantNode);
				descendantNode = descendantNode.parentNode;
			} else if (descendantOffset == descendantNode.length) {
				descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
				descendantNode = descendantNode.parentNode;
			} else {
				newNode = rangy.dom.splitDataNode(descendantNode,
						descendantOffset);
			}
		}
		if (!newNode) {
			newNode = descendantNode.cloneNode(false);
			if (newNode.id) {
				newNode.removeAttribute("id");
			}
			var child;
			while ((child = descendantNode.childNodes[descendantOffset])) {
				newNode.appendChild(child);
			}
			rangy.dom.insertAfter(newNode, descendantNode);
		}
		return (descendantNode == node) ? newNode : splitNodeAt(node,
				newNode.parentNode, rangy.dom.getNodeIndex(newNode));
	}

	function Merge(firstNode) {
		this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
		this.firstTextNode = this.isElementMerge ? firstNode.lastChild
				: firstNode;
		this.textNodes = [ this.firstTextNode ];
	}

	Merge.prototype = {
		doMerge : function() {
			var textBits = [], textNode, parent, text;
			for ( var i = 0, len = this.textNodes.length; i < len; ++i) {
				textNode = this.textNodes[i];
				parent = textNode.parentNode;
				textBits[i] = textNode.data;
				if (i) {
					parent.removeChild(textNode);
					if (!parent.hasChildNodes()) {
						parent.parentNode.removeChild(parent);
					}
				}
			}
			this.firstTextNode.data = text = textBits.join("");
			return text;
		},

		getLength : function() {
			var i = this.textNodes.length, len = 0;
			while (i--) {
				len += this.textNodes[i].length;
			}
			return len;
		},

		toString : function() {
			var textBits = [];
			for ( var i = 0, len = this.textNodes.length; i < len; ++i) {
				textBits[i] = "'" + this.textNodes[i].data + "'";
			}
			return "[Merge(" + textBits.join(",") + ")]";
		}
	};

	function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
		this.tagNames = tagNames || [ defaultTagName ];
		this.cssClass = cssClass || "";
		this.similarClassRegExp = similarClassRegExp;
		this.normalize = normalize;
		this.applyToAnyTagName = false;
	}

	HTMLApplier.prototype = {
		getAncestorWithClass : function(node) {
			var cssClassMatch;
			while (node) {
				cssClassMatch = this.cssClass ? hasClass(node, this.cssClass,
						this.similarClassRegExp) : true;
				if (node.nodeType == wysihtml5.ELEMENT_NODE
						&& rangy.dom.arrayContains(this.tagNames, node.tagName
								.toLowerCase()) && cssClassMatch) {
					return node;
				}
				node = node.parentNode;
			}
			return false;
		},

		// Normalizes nodes after applying a CSS class to a Range.
		postApply : function(textNodes, range) {
			var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];

			var merges = [], currentMerge;

			var rangeStartNode = firstNode, rangeEndNode = lastNode;
			var rangeStartOffset = 0, rangeEndOffset = lastNode.length;

			var textNode, precedingTextNode;

			for ( var i = 0, len = textNodes.length; i < len; ++i) {
				textNode = textNodes[i];
				precedingTextNode = this.getAdjacentMergeableTextNode(
						textNode.parentNode, false);
				if (precedingTextNode) {
					if (!currentMerge) {
						currentMerge = new Merge(precedingTextNode);
						merges.push(currentMerge);
					}
					currentMerge.textNodes.push(textNode);
					if (textNode === firstNode) {
						rangeStartNode = currentMerge.firstTextNode;
						rangeStartOffset = rangeStartNode.length;
					}
					if (textNode === lastNode) {
						rangeEndNode = currentMerge.firstTextNode;
						rangeEndOffset = currentMerge.getLength();
					}
				} else {
					currentMerge = null;
				}
			}

			// Test whether the first node after the range needs merging
			var nextTextNode = this.getAdjacentMergeableTextNode(
					lastNode.parentNode, true);
			if (nextTextNode) {
				if (!currentMerge) {
					currentMerge = new Merge(lastNode);
					merges.push(currentMerge);
				}
				currentMerge.textNodes.push(nextTextNode);
			}

			// Do the merges
			if (merges.length) {
				for (i = 0, len = merges.length; i < len; ++i) {
					merges[i].doMerge();
				}
				// Set the range boundaries
				range.setStart(rangeStartNode, rangeStartOffset);
				range.setEnd(rangeEndNode, rangeEndOffset);
			}
		},

		getAdjacentMergeableTextNode : function(node, forward) {
			var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
			var el = isTextNode ? node.parentNode : node;
			var adjacentNode;
			var propName = forward ? "nextSibling" : "previousSibling";
			if (isTextNode) {
				// Can merge if the node's previous/next sibling is a text node
				adjacentNode = node[propName];
				if (adjacentNode
						&& adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
					return adjacentNode;
				}
			} else {
				// Compare element with its sibling
				adjacentNode = el[propName];
				if (adjacentNode
						&& this.areElementsMergeable(node, adjacentNode)) {
					return adjacentNode[forward ? "firstChild" : "lastChild"];
				}
			}
			return null;
		},

		areElementsMergeable : function(el1, el2) {
			return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "")
					.toLowerCase())
					&& rangy.dom.arrayContains(this.tagNames,
							(el2.tagName || "").toLowerCase())
					&& hasSameClasses(el1, el2)
					&& elementsHaveSameNonClassAttributes(el1, el2);
		},

		createContainer : function(doc) {
			var el = doc.createElement(this.tagNames[0]);
			if (this.cssClass) {
				el.className = this.cssClass;
			}
			return el;
		},

		applyToTextNode : function(textNode) {
			var parent = textNode.parentNode;
			if (parent.childNodes.length == 1
					&& rangy.dom.arrayContains(this.tagNames, parent.tagName
							.toLowerCase())) {
				if (this.cssClass) {
					addClass(parent, this.cssClass, this.similarClassRegExp);
				}
			} else {
				var el = this.createContainer(rangy.dom.getDocument(textNode));
				textNode.parentNode.insertBefore(el, textNode);
				el.appendChild(textNode);
			}
		},

		isRemovable : function(el) {
			return rangy.dom.arrayContains(this.tagNames, el.tagName
					.toLowerCase())
					&& wysihtml5.lang.string(el.className).trim() == this.cssClass;
		},

		undoToTextNode : function(textNode, range, ancestorWithClass) {
			if (!range.containsNode(ancestorWithClass)) {
				// Split out the portion of the ancestor from which we can
				// remove the CSS class
				var ancestorRange = range.cloneRange();
				ancestorRange.selectNode(ancestorWithClass);

				if (ancestorRange.isPointInRange(range.endContainer,
						range.endOffset)
						&& isSplitPoint(range.endContainer, range.endOffset)) {
					splitNodeAt(ancestorWithClass, range.endContainer,
							range.endOffset);
					range.setEndAfter(ancestorWithClass);
				}
				if (ancestorRange.isPointInRange(range.startContainer,
						range.startOffset)
						&& isSplitPoint(range.startContainer, range.startOffset)) {
					ancestorWithClass = splitNodeAt(ancestorWithClass,
							range.startContainer, range.startOffset);
				}
			}

			if (this.similarClassRegExp) {
				removeClass(ancestorWithClass, this.similarClassRegExp);
			}
			if (this.isRemovable(ancestorWithClass)) {
				replaceWithOwnChildren(ancestorWithClass);
			}
		},

		applyToRange : function(range) {
			var textNodes = range.getNodes([ wysihtml5.TEXT_NODE ]);
			if (!textNodes.length) {
				try {
					var node = this
							.createContainer(range.endContainer.ownerDocument);
					range.surroundContents(node);
					this.selectNode(range, node);
					return;
				} catch (e) {
				}
			}

			range.splitBoundaries();
			textNodes = range.getNodes([ wysihtml5.TEXT_NODE ]);

			if (textNodes.length) {
				var textNode;

				for ( var i = 0, len = textNodes.length; i < len; ++i) {
					textNode = textNodes[i];
					if (!this.getAncestorWithClass(textNode)) {
						this.applyToTextNode(textNode);
					}
				}

				range.setStart(textNodes[0], 0);
				textNode = textNodes[textNodes.length - 1];
				range.setEnd(textNode, textNode.length);

				if (this.normalize) {
					this.postApply(textNodes, range);
				}
			}
		},

		undoToRange : function(range) {
			var textNodes = range.getNodes([ wysihtml5.TEXT_NODE ]), textNode, ancestorWithClass;
			if (textNodes.length) {
				range.splitBoundaries();
				textNodes = range.getNodes([ wysihtml5.TEXT_NODE ]);
			} else {
				var doc = range.endContainer.ownerDocument, node = doc
						.createTextNode(wysihtml5.INVISIBLE_SPACE);
				range.insertNode(node);
				range.selectNode(node);
				textNodes = [ node ];
			}

			for ( var i = 0, len = textNodes.length; i < len; ++i) {
				textNode = textNodes[i];
				ancestorWithClass = this.getAncestorWithClass(textNode);
				if (ancestorWithClass) {
					this.undoToTextNode(textNode, range, ancestorWithClass);
				}
			}

			if (len == 1) {
				this.selectNode(range, textNodes[0]);
			} else {
				range.setStart(textNodes[0], 0);
				textNode = textNodes[textNodes.length - 1];
				range.setEnd(textNode, textNode.length);

				if (this.normalize) {
					this.postApply(textNodes, range);
				}
			}
		},

		selectNode : function(range, node) {
			var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML
					: true, content = isElement ? node.innerHTML : node.data, isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE);

			if (isEmpty && isElement && canHaveHTML) {
				// Make sure that caret is visible in node by inserting a zero
				// width no breaking space
				try {
					node.innerHTML = wysihtml5.INVISIBLE_SPACE;
				} catch (e) {
				}
			}
			range.selectNodeContents(node);
			if (isEmpty && isElement) {
				range.collapse(false);
			} else if (isEmpty) {
				range.setStartAfter(node);
				range.setEndAfter(node);
			}
		},

		getTextSelectedByRange : function(textNode, range) {
			var textRange = range.cloneRange();
			textRange.selectNodeContents(textNode);

			var intersectionRange = textRange.intersection(range);
			var text = intersectionRange ? intersectionRange.toString() : "";
			textRange.detach();

			return text;
		},

		isAppliedToRange : function(range) {
			var ancestors = [], ancestor, textNodes = range
					.getNodes([ wysihtml5.TEXT_NODE ]);
			if (!textNodes.length) {
				ancestor = this.getAncestorWithClass(range.startContainer);
				return ancestor ? [ ancestor ] : false;
			}

			for ( var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
				selectedText = this.getTextSelectedByRange(textNodes[i], range);
				ancestor = this.getAncestorWithClass(textNodes[i]);
				if (selectedText != "" && !ancestor) {
					return false;
				} else {
					ancestors.push(ancestor);
				}
			}
			return ancestors;
		},

		toggleRange : function(range) {
			if (this.isAppliedToRange(range)) {
				this.undoToRange(range);
			} else {
				this.applyToRange(range);
			}
		}
	};

	wysihtml5.selection.HTMLApplier = HTMLApplier;

})(wysihtml5, rangy);
/**
 * Rich Text Query/Formatting Commands
 * 
 * @example var commands = new wysihtml5.Commands(editor);
 */
wysihtml5.Commands = Base.extend(
/** @scope wysihtml5.Commands.prototype */
{
	constructor : function(editor) {
		this.editor = editor;
		this.composer = editor.composer;
		this.doc = this.composer.doc;
	},

	/**
	 * Check whether the browser supports the given command
	 * 
	 * @param {String}
	 *            command The command string which to check (eg. "bold",
	 *            "italic", "insertUnorderedList")
	 * @example commands.supports("createLink");
	 */
	support : function(command) {
		return wysihtml5.browser.supportsCommand(this.doc, command);
	},

	/**
	 * Check whether the browser supports the given command
	 * 
	 * @param {String}
	 *            command The command string which to execute (eg. "bold",
	 *            "italic", "insertUnorderedList")
	 * @param {String}
	 *            [value] The command value parameter, needed for some commands
	 *            ("createLink", "insertImage", ...), optional for commands that
	 *            don't require one ("bold", "underline", ...)
	 * @example commands.exec("insertImage",
	 *          "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
	 */
	exec : function(command, value) {
		var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(
				arguments).get(), method = obj && obj.exec, result = null;

		this.editor.fire("beforecommand:composer");

		if (method) {
			args.unshift(this.composer);
			result = method.apply(obj, args);
		} else {
			try {
				// try/catch for buggy firefox
				result = this.doc.execCommand(command, false, value);
			} catch (e) {
			}
		}

		this.editor.fire("aftercommand:composer");
		return result;
	},

	/**
	 * Check whether the current command is active If the caret is within a bold
	 * text, then calling this with command "bold" should return true
	 * 
	 * @param {String}
	 *            command The command string which to check (eg. "bold",
	 *            "italic", "insertUnorderedList")
	 * @param {String}
	 *            [commandValue] The command value parameter (eg. for
	 *            "insertImage" the image src)
	 * @return {Boolean} Whether the command is active
	 * @example var isCurrentSelectionBold = commands.state("bold");
	 */
	state : function(command, commandValue) {
		var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(
				arguments).get(), method = obj && obj.state;
		if (method) {
			args.unshift(this.composer);
			return method.apply(obj, args);
		} else {
			try {
				// try/catch for buggy firefox
				return this.doc.queryCommandState(command);
			} catch (e) {
				return false;
			}
		}
	},

	/**
	 * Get the current command's value
	 * 
	 * @param {String}
	 *            command The command string which to check (eg. "formatBlock")
	 * @return {String} The command value
	 * @example var currentBlockElement = commands.value("formatBlock");
	 */
	value : function(command) {
		var obj = wysihtml5.commands[command], method = obj && obj.value;
		if (method) {
			return method.call(obj, this.composer, command);
		} else {
			try {
				// try/catch for buggy firefox
				return this.doc.queryCommandValue(command);
			} catch (e) {
				return null;
			}
		}
	}
});
(function(wysihtml5) {
	var undef;

	wysihtml5.commands.bold = {
		exec : function(composer, command) {
			return wysihtml5.commands.formatInline.exec(composer, command, "b");
		},

		state : function(composer, command, color) {
			// element.ownerDocument.queryCommandState("bold") results:
			// firefox: only <b>
			// chrome: <b>, <strong>, <h1>, <h2>, ...
			// ie: <b>, <strong>
			// opera: <b>, <strong>
			return wysihtml5.commands.formatInline
					.state(composer, command, "b");
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);

(function(wysihtml5) {
	var undef, NODE_NAME = "A", dom = wysihtml5.dom;

	function _removeFormat(composer, anchors) {
		var length = anchors.length, i = 0, anchor, codeElement, textContent;
		for (; i < length; i++) {
			anchor = anchors[i];
			codeElement = dom.getParentElement(anchor, {
				nodeName : "code"
			});
			textContent = dom.getTextContent(anchor);

			// if <a> contains url-like text content, rename it to <code> to
			// prevent re-autolinking
			// else replace <a> with its childNodes
			if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
				// <code> element is used to prevent later auto-linking of the
				// content
				codeElement = dom.renameElement(anchor, "code");
			} else {
				dom.replaceWithChildNodes(anchor);
			}
		}
	}

	function _format(composer, attributes) {
		var doc = composer.doc, tempClass = "_wysihtml5-temp-" + (+new Date()), tempClassRegExp = /non-matching-class/g, i = 0, length, anchors, anchor, hasElementChild, isEmpty, elementToSetCaretAfter, textContent, whiteSpace, j;
		wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME,
				tempClass, tempClassRegExp);
		anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
		length = anchors.length;
		for (; i < length; i++) {
			anchor = anchors[i];
			anchor.removeAttribute("class");
			for (j in attributes) {
				anchor.setAttribute(j, attributes[j]);
			}
		}

		elementToSetCaretAfter = anchor;
		if (length === 1) {
			textContent = dom.getTextContent(anchor);
			hasElementChild = !!anchor.querySelector("*");
			isEmpty = textContent === ""
					|| textContent === wysihtml5.INVISIBLE_SPACE;
			if (!hasElementChild && isEmpty) {
				dom.setTextContent(anchor, attributes.text || anchor.href);
				whiteSpace = doc.createTextNode(" ");
				composer.selection.setAfter(anchor);
				composer.selection.insertNode(whiteSpace);
				elementToSetCaretAfter = whiteSpace;
			}
		}
		composer.selection.setAfter(elementToSetCaretAfter);
	}

	wysihtml5.commands.createLink = {
		/**
		 * TODO: Use HTMLApplier or formatInline here
		 * 
		 * Turns selection into a link If selection is already a link, it
		 * removes the link and wraps it with a <code> element
		 * The <code> element is needed to avoid auto linking
		 * 
		 * @example
		 *    // either ...
		 *    wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
		 *    // ... or ...
		 *    wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
		 */
		exec : function(composer, command, value) {
			var anchors = this.state(composer, command);
			if (anchors) {
				// Selection contains links
				composer.selection.executeAndRestore(function() {
					_removeFormat(composer, anchors);
				});
			} else {
				// Create links
				value = typeof (value) === "object" ? value : {
					href : value
				};
				_format(composer, value);
			}
		},

		state : function(composer, command) {
			return wysihtml5.commands.formatInline
					.state(composer, command, "A");
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
/**
 * document.execCommand("fontSize") will create either inline styles (firefox,
 * chrome) or use font tags which we don't want Instead we set a css class
 */
(function(wysihtml5) {
	var undef, REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;

	wysihtml5.commands.fontSize = {
		exec : function(composer, command, size) {
			return wysihtml5.commands.formatInline.exec(composer, command,
					"span", "wysiwyg-font-size-" + size, REG_EXP);
		},

		state : function(composer, command, size) {
			return wysihtml5.commands.formatInline.state(composer, command,
					"span", "wysiwyg-font-size-" + size, REG_EXP);
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
/**
 * document.execCommand("foreColor") will create either inline styles (firefox,
 * chrome) or use font tags which we don't want Instead we set a css class
 */
(function(wysihtml5) {
	var undef, REG_EXP = /wysiwyg-color-[a-z]+/g;

	wysihtml5.commands.foreColor = {
		exec : function(composer, command, color) {
			return wysihtml5.commands.formatInline.exec(composer, command,
					"span", "wysiwyg-color-" + color, REG_EXP);
		},

		state : function(composer, command, color) {
			return wysihtml5.commands.formatInline.state(composer, command,
					"span", "wysiwyg-color-" + color, REG_EXP);
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef, dom = wysihtml5.dom, DEFAULT_NODE_NAME = "DIV",
	// Following elements are grouped
	// when the caret is within a H1 and the H4 is invoked, the H1 should turn
	// into H4
	// instead of creating a H4 within a H1 which would result in semantically
	// invalid html
	BLOCK_ELEMENTS_GROUP = [ "H1", "H2", "H3", "H4", "H5", "H6", "P",
			"BLOCKQUOTE", DEFAULT_NODE_NAME ];

	/**
	 * Remove similiar classes (based on classRegExp) and add the desired class
	 * name
	 */
	function _addClass(element, className, classRegExp) {
		if (element.className) {
			_removeClass(element, classRegExp);
			element.className += " " + className;
		} else {
			element.className = className;
		}
	}

	function _removeClass(element, classRegExp) {
		element.className = element.className.replace(classRegExp, "");
	}

	/**
	 * Check whether given node is a text node and whether it's empty
	 */
	function _isBlankTextNode(node) {
		return node.nodeType === wysihtml5.TEXT_NODE
				&& !wysihtml5.lang.string(node.data).trim();
	}

	/**
	 * Returns previous sibling node that is not a blank text node
	 */
	function _getPreviousSiblingThatIsNotBlank(node) {
		var previousSibling = node.previousSibling;
		while (previousSibling && _isBlankTextNode(previousSibling)) {
			previousSibling = previousSibling.previousSibling;
		}
		return previousSibling;
	}

	/**
	 * Returns next sibling node that is not a blank text node
	 */
	function _getNextSiblingThatIsNotBlank(node) {
		var nextSibling = node.nextSibling;
		while (nextSibling && _isBlankTextNode(nextSibling)) {
			nextSibling = nextSibling.nextSibling;
		}
		return nextSibling;
	}

	/**
	 * Adds line breaks before and after the given node if the previous and next
	 * siblings aren't already causing a visual line break (block element or
	 * <br>)
	 */
	function _addLineBreakBeforeAndAfter(node) {
		var doc = node.ownerDocument, nextSibling = _getNextSiblingThatIsNotBlank(node), previousSibling = _getPreviousSiblingThatIsNotBlank(node);

		if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
			node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
		}
		if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
			node.parentNode.insertBefore(doc.createElement("br"), node);
		}
	}

	/**
	 * Removes line breaks before and after the given node
	 */
	function _removeLineBreakBeforeAndAfter(node) {
		var nextSibling = _getNextSiblingThatIsNotBlank(node), previousSibling = _getPreviousSiblingThatIsNotBlank(node);

		if (nextSibling && _isLineBreak(nextSibling)) {
			nextSibling.parentNode.removeChild(nextSibling);
		}
		if (previousSibling && _isLineBreak(previousSibling)) {
			previousSibling.parentNode.removeChild(previousSibling);
		}
	}

	function _removeLastChildIfLineBreak(node) {
		var lastChild = node.lastChild;
		if (lastChild && _isLineBreak(lastChild)) {
			lastChild.parentNode.removeChild(lastChild);
		}
	}

	function _isLineBreak(node) {
		return node.nodeName === "BR";
	}

	/**
	 * Checks whether the elment causes a visual line break (<br>
	 * or block elements)
	 */
	function _isLineBreakOrBlockElement(element) {
		if (_isLineBreak(element)) {
			return true;
		}

		if (dom.getStyle("display").from(element) === "block") {
			return true;
		}

		return false;
	}

	/**
	 * Execute native query command and if necessary modify the inserted node's
	 * className
	 */
	function _execCommand(doc, command, nodeName, className) {
		if (className) {
			var eventListener = dom.observe(doc, "DOMNodeInserted", function(
					event) {
				var target = event.target, displayStyle;
				if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
					return;
				}
				displayStyle = dom.getStyle("display").from(target);
				if (displayStyle.substr(0, 6) !== "inline") {
					// Make sure that only block elements receive the given
					// class
					target.className += " " + className;
				}
			});
		}
		doc.execCommand(command, false, nodeName);
		if (eventListener) {
			eventListener.stop();
		}
	}

	function _selectLineAndWrap(composer, element) {
		composer.selection.selectLine();
		composer.selection.surround(element);
		_removeLineBreakBeforeAndAfter(element);
		_removeLastChildIfLineBreak(element);
		composer.selection.selectNode(element);
	}

	function _hasClasses(element) {
		return !!wysihtml5.lang.string(element.className).trim();
	}

	wysihtml5.commands.formatBlock = {
		exec : function(composer, command, nodeName, className, classRegExp) {
			var doc = composer.doc, blockElement = this.state(composer,
					command, nodeName, className, classRegExp), selectedNode;

			nodeName = typeof (nodeName) === "string" ? nodeName.toUpperCase()
					: nodeName;

			if (blockElement) {
				composer.selection
						.executeAndRestoreSimple(function() {
							if (classRegExp) {
								_removeClass(blockElement, classRegExp);
							}
							var hasClasses = _hasClasses(blockElement);
							if (!hasClasses
									&& blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {
								// Insert a line break afterwards and
								// beforewards when there are siblings
								// that are not of type line break or block
								// element
								_addLineBreakBeforeAndAfter(blockElement);
								dom.replaceWithChildNodes(blockElement);
							} else if (hasClasses) {
								// Make sure that styling is kept by renaming
								// the element to <div> and copying over the
								// class name
								dom.renameElement(blockElement,
										DEFAULT_NODE_NAME);
							}
						});
				return;
			}

			// Find similiar block element and rename it (<h2 class="foo"></h2>
			// => <h1 class="foo"></h1>)
			if (nodeName === null
					|| wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(
							nodeName)) {
				selectedNode = composer.selection.getSelectedNode();
				blockElement = dom.getParentElement(selectedNode, {
					nodeName : BLOCK_ELEMENTS_GROUP
				});

				if (blockElement) {
					composer.selection.executeAndRestoreSimple(function() {
						// Rename current block element to new block element and
						// add class
						if (nodeName) {
							blockElement = dom.renameElement(blockElement,
									nodeName);
						}
						if (className) {
							_addClass(blockElement, className, classRegExp);
						}
					});
					return;
				}
			}

			if (composer.commands.support(command)) {
				_execCommand(doc, command, nodeName || DEFAULT_NODE_NAME,
						className);
				return;
			}

			blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
			if (className) {
				blockElement.className = className;
			}
			_selectLineAndWrap(composer, blockElement);
		},

		state : function(composer, command, nodeName, className, classRegExp) {
			nodeName = typeof (nodeName) === "string" ? nodeName.toUpperCase()
					: nodeName;
			var selectedNode = composer.selection.getSelectedNode();
			return dom.getParentElement(selectedNode, {
				nodeName : nodeName,
				className : className,
				classRegExp : classRegExp
			});
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
/**
 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
 * 
 * #1 caret in unformatted text: abcdefg| output: abcdefg<b>|</b>
 * 
 * #2 unformatted text selected: abc|deg|h output: abc<b>|deg|</b>h
 * 
 * #3 unformatted text selected across boundaries: ab|c <span>defg|h</span>
 * output: ab<b>|c </b><span><b>defg</b>|h</span>
 * 
 * #4 formatted text entirely selected <b>|abc|</b> output: |abc|
 * 
 * #5 formatted text partially selected <b>ab|c|</b> output: <b>ab</b>|c|
 * 
 * #6 formatted text selected across boundaries <span>ab|c</span> <b>de|fgh</b>
 * output: <span>ab|c</span> de|<b>fgh</b>
 */
(function(wysihtml5) {
	var undef,
	// Treat <b> as <strong> and vice versa
	ALIAS_MAPPING = {
		"strong" : "b",
		"em" : "i",
		"b" : "strong",
		"i" : "em"
	}, htmlApplier = {};

	function _getTagNames(tagName) {
		var alias = ALIAS_MAPPING[tagName];
		return alias ? [ tagName.toLowerCase(), alias.toLowerCase() ]
				: [ tagName.toLowerCase() ];
	}

	function _getApplier(tagName, className, classRegExp) {
		var identifier = tagName + ":" + className;
		if (!htmlApplier[identifier]) {
			htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(
					_getTagNames(tagName), className, classRegExp, true);
		}
		return htmlApplier[identifier];
	}

	wysihtml5.commands.formatInline = {
		exec : function(composer, command, tagName, className, classRegExp) {
			var range = composer.selection.getRange();
			if (!range) {
				return false;
			}
			_getApplier(tagName, className, classRegExp).toggleRange(range);
			composer.selection.setSelection(range);
		},

		state : function(composer, command, tagName, className, classRegExp) {
			var doc = composer.doc, aliasTagName = ALIAS_MAPPING[tagName]
					|| tagName, range;

			// Check whether the document contains a node with the desired
			// tagName
			if (!wysihtml5.dom.hasElementWithTagName(doc, tagName)
					&& !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
				return false;
			}

			// Check whether the document contains a node with the desired
			// className
			if (className
					&& !wysihtml5.dom.hasElementWithClassName(doc, className)) {
				return false;
			}

			range = composer.selection.getRange();
			if (!range) {
				return false;
			}

			return _getApplier(tagName, className, classRegExp)
					.isAppliedToRange(range);
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef;

	wysihtml5.commands.insertHTML = {
		exec : function(composer, command, html) {
			if (composer.commands.support(command)) {
				composer.doc.execCommand(command, false, html);
			} else {
				composer.selection.insertHTML(html);
			}
		},

		state : function() {
			return false;
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var NODE_NAME = "IMG";

	wysihtml5.commands.insertImage = {
		/**
		 * Inserts an <img> If selection is already an image link, it removes it
		 * 
		 * @example // either ... wysihtml5.commands.insertImage.exec(composer,
		 *          "insertImage", "http://www.google.de/logo.jpg"); // ... or
		 *          ... wysihtml5.commands.insertImage.exec(composer,
		 *          "insertImage", { src: "http://www.google.de/logo.jpg",
		 *          title: "foo" });
		 */
		exec : function(composer, command, value) {
			value = typeof (value) === "object" ? value : {
				src : value
			};

			var doc = composer.doc, image = this.state(composer), textNode, i, parent;

			if (image) {
				// Image already selected, set the caret before it and delete it
				composer.selection.setBefore(image);
				parent = image.parentNode;
				parent.removeChild(image);

				// and it's parent <a> too if it hasn't got any other relevant
				// child nodes
				wysihtml5.dom.removeEmptyTextNodes(parent);
				if (parent.nodeName === "A" && !parent.firstChild) {
					composer.selection.setAfter(parent);
					parent.parentNode.removeChild(parent);
				}

				// firefox and ie sometimes don't remove the image handles, even
				// though the image got removed
				wysihtml5.quirks.redraw(composer.element);
				return;
			}

			image = doc.createElement(NODE_NAME);

			for (i in value) {
				image[i] = value[i];
			}

			composer.selection.insertNode(image);
			if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
				textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
				composer.selection.insertNode(textNode);
				composer.selection.setAfter(textNode);
			} else {
				composer.selection.setAfter(image);
			}
		},

		state : function(composer) {
			var doc = composer.doc, selectedNode, text, imagesInSelection;

			if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
				return false;
			}

			selectedNode = composer.selection.getSelectedNode();
			if (!selectedNode) {
				return false;
			}

			if (selectedNode.nodeName === NODE_NAME) {
				// This works perfectly in IE
				return selectedNode;
			}

			if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
				return false;
			}

			text = composer.selection.getText();
			text = wysihtml5.lang.string(text).trim();
			if (text) {
				return false;
			}

			imagesInSelection = composer.selection.getNodes(
					wysihtml5.ELEMENT_NODE, function(node) {
						return node.nodeName === "IMG";
					});

			if (imagesInSelection.length !== 1) {
				return false;
			}

			return imagesInSelection[0];
		},

		value : function(composer) {
			var image = this.state(composer);
			return image && image.src;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef, LINE_BREAK = "<br>"
			+ (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");

	wysihtml5.commands.insertLineBreak = {
		exec : function(composer, command) {
			if (composer.commands.support(command)) {
				composer.doc.execCommand(command, false, null);
				if (!wysihtml5.browser.autoScrollsToCaret()) {
					composer.selection.scrollIntoView();
				}
			} else {
				composer.commands.exec("insertHTML", LINE_BREAK);
			}
		},

		state : function() {
			return false;
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef;

	wysihtml5.commands.insertOrderedList = {
		exec : function(composer, command) {
			var doc = composer.doc, selectedNode = composer.selection
					.getSelectedNode(), list = wysihtml5.dom.getParentElement(
					selectedNode, {
						nodeName : "OL"
					}), otherList = wysihtml5.dom.getParentElement(
					selectedNode, {
						nodeName : "UL"
					}), tempClassName = "_wysihtml5-temp-"
					+ new Date().getTime(), isEmpty, tempElement;

			if (composer.commands.support(command)) {
				doc.execCommand(command, false, null);
				return;
			}

			if (list) {
				// Unwrap list
				// <ol><li>foo</li><li>bar</li></ol>
				// becomes:
				// foo<br>bar<br>
				composer.selection.executeAndRestoreSimple(function() {
					wysihtml5.dom.resolveList(list);
				});
			} else if (otherList) {
				// Turn an unordered list into an ordered list
				// <ul><li>foo</li><li>bar</li></ul>
				// becomes:
				// <ol><li>foo</li><li>bar</li></ol>
				composer.selection.executeAndRestoreSimple(function() {
					wysihtml5.dom.renameElement(otherList, "ol");
				});
			} else {
				// Create list
				composer.commands.exec("formatBlock", "div", tempClassName);
				tempElement = doc.querySelector("." + tempClassName);
				isEmpty = tempElement.innerHTML === ""
						|| tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
				composer.selection.executeAndRestoreSimple(function() {
					list = wysihtml5.dom.convertToList(tempElement, "ol");
				});
				if (isEmpty) {
					composer.selection.selectNode(list.querySelector("li"));
				}
			}
		},

		state : function(composer) {
			var selectedNode = composer.selection.getSelectedNode();
			return wysihtml5.dom.getParentElement(selectedNode, {
				nodeName : "OL"
			});
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef;

	wysihtml5.commands.insertUnorderedList = {
		exec : function(composer, command) {
			var doc = composer.doc, selectedNode = composer.selection
					.getSelectedNode(), list = wysihtml5.dom.getParentElement(
					selectedNode, {
						nodeName : "UL"
					}), otherList = wysihtml5.dom.getParentElement(
					selectedNode, {
						nodeName : "OL"
					}), tempClassName = "_wysihtml5-temp-"
					+ new Date().getTime(), isEmpty, tempElement;

			if (composer.commands.support(command)) {
				doc.execCommand(command, false, null);
				return;
			}

			if (list) {
				// Unwrap list
				// <ul><li>foo</li><li>bar</li></ul>
				// becomes:
				// foo<br>bar<br>
				composer.selection.executeAndRestoreSimple(function() {
					wysihtml5.dom.resolveList(list);
				});
			} else if (otherList) {
				// Turn an ordered list into an unordered list
				// <ol><li>foo</li><li>bar</li></ol>
				// becomes:
				// <ul><li>foo</li><li>bar</li></ul>
				composer.selection.executeAndRestoreSimple(function() {
					wysihtml5.dom.renameElement(otherList, "ul");
				});
			} else {
				// Create list
				composer.commands.exec("formatBlock", "div", tempClassName);
				tempElement = doc.querySelector("." + tempClassName);
				isEmpty = tempElement.innerHTML === ""
						|| tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
				composer.selection.executeAndRestoreSimple(function() {
					list = wysihtml5.dom.convertToList(tempElement, "ul");
				});
				if (isEmpty) {
					composer.selection.selectNode(list.querySelector("li"));
				}
			}
		},

		state : function(composer) {
			var selectedNode = composer.selection.getSelectedNode();
			return wysihtml5.dom.getParentElement(selectedNode, {
				nodeName : "UL"
			});
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef;

	wysihtml5.commands.italic = {
		exec : function(composer, command) {
			return wysihtml5.commands.formatInline.exec(composer, command, "i");
		},

		state : function(composer, command, color) {
			// element.ownerDocument.queryCommandState("italic") results:
			// firefox: only <i>
			// chrome: <i>, <em>, <blockquote>, ...
			// ie: <i>, <em>
			// opera: only <i>
			return wysihtml5.commands.formatInline
					.state(composer, command, "i");
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef, CLASS_NAME = "wysiwyg-text-align-center", REG_EXP = /wysiwyg-text-align-[a-z]+/g;

	wysihtml5.commands.justifyCenter = {
		exec : function(composer, command) {
			return wysihtml5.commands.formatBlock.exec(composer, "formatBlock",
					null, CLASS_NAME, REG_EXP);
		},

		state : function(composer, command) {
			return wysihtml5.commands.formatBlock.state(composer,
					"formatBlock", null, CLASS_NAME, REG_EXP);
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef, CLASS_NAME = "wysiwyg-text-align-left", REG_EXP = /wysiwyg-text-align-[a-z]+/g;

	wysihtml5.commands.justifyLeft = {
		exec : function(composer, command) {
			return wysihtml5.commands.formatBlock.exec(composer, "formatBlock",
					null, CLASS_NAME, REG_EXP);
		},

		state : function(composer, command) {
			return wysihtml5.commands.formatBlock.state(composer,
					"formatBlock", null, CLASS_NAME, REG_EXP);
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef, CLASS_NAME = "wysiwyg-text-align-right", REG_EXP = /wysiwyg-text-align-[a-z]+/g;

	wysihtml5.commands.justifyRight = {
		exec : function(composer, command) {
			return wysihtml5.commands.formatBlock.exec(composer, "formatBlock",
					null, CLASS_NAME, REG_EXP);
		},

		state : function(composer, command) {
			return wysihtml5.commands.formatBlock.state(composer,
					"formatBlock", null, CLASS_NAME, REG_EXP);
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
(function(wysihtml5) {
	var undef;
	wysihtml5.commands.underline = {
		exec : function(composer, command) {
			return wysihtml5.commands.formatInline.exec(composer, command, "u");
		},

		state : function(composer, command) {
			return wysihtml5.commands.formatInline
					.state(composer, command, "u");
		},

		value : function() {
			return undef;
		}
	};
})(wysihtml5);
/**
 * Undo Manager for wysihtml5 slightly inspired by
 * http://rniwa.com/editing/undomanager.html#the-undomanager-interface
 */
(function(wysihtml5) {
	var Z_KEY = 90, Y_KEY = 89, BACKSPACE_KEY = 8, DELETE_KEY = 46, MAX_HISTORY_ENTRIES = 40, UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">'
			+ wysihtml5.INVISIBLE_SPACE + '</span>', REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">'
			+ wysihtml5.INVISIBLE_SPACE + '</span>', dom = wysihtml5.dom;

	function cleanTempElements(doc) {
		var tempElement;
		while (tempElement = doc.querySelector("._wysihtml5-temp")) {
			tempElement.parentNode.removeChild(tempElement);
		}
	}

	wysihtml5.UndoManager = wysihtml5.lang.Dispatcher
			.extend(
			/** @scope wysihtml5.UndoManager.prototype */
			{
				constructor : function(editor) {
					this.editor = editor;
					this.composer = editor.composer;
					this.element = this.composer.element;
					this.history = [ this.composer.getValue() ];
					this.position = 1;

					// Undo manager currently only supported in browsers who
					// have the insertHTML command (not IE)
					if (this.composer.commands.support("insertHTML")) {
						this._observe();
					}
				},

				_observe : function() {
					var that = this, doc = this.composer.sandbox.getDocument(), lastKey;

					// Catch CTRL+Z and CTRL+Y
					dom
							.observe(
									this.element,
									"keydown",
									function(event) {
										if (event.altKey
												|| (!event.ctrlKey && !event.metaKey)) {
											return;
										}

										var keyCode = event.keyCode, isUndo = keyCode === Z_KEY
												&& !event.shiftKey, isRedo = (keyCode === Z_KEY && event.shiftKey)
												|| (keyCode === Y_KEY);

										if (isUndo) {
											that.undo();
											event.preventDefault();
										} else if (isRedo) {
											that.redo();
											event.preventDefault();
										}
									});

					// Catch delete and backspace
					dom.observe(this.element, "keydown",
							function(event) {
								var keyCode = event.keyCode;
								if (keyCode === lastKey) {
									return;
								}

								lastKey = keyCode;

								if (keyCode === BACKSPACE_KEY
										|| keyCode === DELETE_KEY) {
									that.transact();
								}
							});

					// Now this is very hacky:
					// These days browsers don't offer a undo/redo event which
					// we could hook into
					// to be notified when the user hits undo/redo in the
					// contextmenu.
					// Therefore we simply insert two elements as soon as the
					// contextmenu gets opened.
					// The last element being inserted will be immediately be
					// removed again by a exexCommand("undo")
					// => When the second element appears in the dom tree then
					// we know the user clicked "redo" in the context menu
					// => When the first element disappears from the dom tree
					// then we know the user clicked "undo" in the context menu
					if (wysihtml5.browser.hasUndoInContextMenu()) {
						var interval, observed, cleanUp = function() {
							cleanTempElements(doc);
							clearInterval(interval);
						};

						dom
								.observe(
										this.element,
										"contextmenu",
										function() {
											cleanUp();
											that.composer.selection
													.executeAndRestoreSimple(function() {
														if (that.element.lastChild) {
															that.composer.selection
																	.setAfter(that.element.lastChild);
														}

														// enable undo button in
														// context menu
														doc.execCommand(
																"insertHTML",
																false,
																UNDO_HTML);
														// enable redo button in
														// context menu
														doc.execCommand(
																"insertHTML",
																false,
																REDO_HTML);
														doc.execCommand("undo",
																false, null);
													});

											interval = setInterval(
													function() {
														if (doc
																.getElementById("_wysihtml5-redo")) {
															cleanUp();
															that.redo();
														} else if (!doc
																.getElementById("_wysihtml5-undo")) {
															cleanUp();
															that.undo();
														}
													}, 400);

											if (!observed) {
												observed = true;
												dom.observe(document,
														"mousedown", cleanUp);
												dom
														.observe(doc, [
																"mousedown",
																"paste", "cut",
																"copy" ],
																cleanUp);
											}
										});
					}

					this.editor.observe("newword:composer", function() {
						that.transact();
					})

					.observe("beforecommand:composer", function() {
						that.transact();
					});
				},

				transact : function() {
					var previousHtml = this.history[this.position - 1], currentHtml = this.composer
							.getValue();

					if (currentHtml == previousHtml) {
						return;
					}

					var length = this.history.length = this.position;
					if (length > MAX_HISTORY_ENTRIES) {
						this.history.shift();
						this.position--;
					}

					this.position++;
					this.history.push(currentHtml);
				},

				undo : function() {
					this.transact();

					if (this.position <= 1) {
						return;
					}

					this.set(this.history[--this.position - 1]);
					this.editor.fire("undo:composer");
				},

				redo : function() {
					if (this.position >= this.history.length) {
						return;
					}

					this.set(this.history[++this.position - 1]);
					this.editor.fire("redo:composer");
				},

				set : function(html) {
					this.composer.setValue(html);
					this.editor.focus(true);
				}
			});
})(wysihtml5);
/**
 * TODO: the following methods still need unit test coverage
 */
wysihtml5.views.View = Base
		.extend(
		/** @scope wysihtml5.views.View.prototype */
		{
			constructor : function(parent, textareaElement, config) {
				this.parent = parent;
				this.element = textareaElement;
				this.config = config;

				this._observeViewChange();
			},

			_observeViewChange : function() {
				var that = this;
				this.parent.observe("beforeload", function() {
					that.parent.observe("change_view", function(view) {
						if (view === that.name) {
							that.parent.currentView = that;
							that.show();
							// Using tiny delay here to make sure that the
							// placeholder is set before focusing
							setTimeout(function() {
								that.focus();
							}, 0);
						} else {
							that.hide();
						}
					});
				});
			},

			focus : function() {
				if (this.element.ownerDocument.querySelector(":focus") === this.element) {
					return;
				}

				try {
					this.element.focus();
				} catch (e) {
				}
			},

			hide : function() {
				this.element.style.display = "none";
			},

			show : function() {
				this.element.style.display = "";
			},

			disable : function() {
				this.element.setAttribute("disabled", "disabled");
			},

			enable : function() {
				this.element.removeAttribute("disabled");
			}
		});
(function(wysihtml5) {
	var dom = wysihtml5.dom, browser = wysihtml5.browser;

	wysihtml5.views.Composer = wysihtml5.views.View
			.extend(
			/** @scope wysihtml5.views.Composer.prototype */
			{
				name : "composer",

				// Needed for firefox in order to display a proper caret in an
				// empty contentEditable
				CARET_HACK : "<br>",

				constructor : function(parent, textareaElement, config) {
					this.base(parent, textareaElement, config);
					this.textarea = this.parent.textarea;
					this._initSandbox();
				},

				clear : function() {
					this.element.innerHTML = browser
							.displaysCaretInEmptyContentEditableCorrectly() ? ""
							: this.CARET_HACK;
				},

				getValue : function(parse) {
					var value = this.isEmpty() ? "" : wysihtml5.quirks
							.getCorrectInnerHTML(this.element);

					if (parse) {
						value = this.parent.parse(value);
					}

					// Replace all "zero width no breaking space" chars
					// which are used as hacks to enable some functionalities
					// Also remove all CARET hacks that somehow got left
					value = wysihtml5.lang.string(value).replace(
							wysihtml5.INVISIBLE_SPACE).by("");

					return value;
				},

				setValue : function(html, parse) {
					if (parse) {
						html = this.parent.parse(html);
					}
					this.element.innerHTML = html;
				},

				show : function() {
					this.iframe.style.display = this._displayStyle || "";

					// Firefox needs this, otherwise contentEditable becomes
					// uneditable
					this.disable();
					this.enable();
				},

				hide : function() {
					this._displayStyle = dom.getStyle("display").from(
							this.iframe);
					if (this._displayStyle === "none") {
						this._displayStyle = null;
					}
					this.iframe.style.display = "none";
				},

				disable : function() {
					this.element.removeAttribute("contentEditable");
					this.base();
				},

				enable : function() {
					this.element.setAttribute("contentEditable", "true");
					this.base();
				},

				focus : function(setToEnd) {
					// IE 8 fires the focus event after .focus()
					// This is needed by our simulate_placeholder.js to work
					// therefore we clear it ourselves this time
					if (wysihtml5.browser.doesAsyncFocus()
							&& this.hasPlaceholderSet()) {
						this.clear();
					}

					this.base();

					var lastChild = this.element.lastChild;
					if (setToEnd && lastChild) {
						if (lastChild.nodeName === "BR") {
							this.selection.setBefore(this.element.lastChild);
						} else {
							this.selection.setAfter(this.element.lastChild);
						}
					}
				},

				getTextContent : function() {
					return dom.getTextContent(this.element);
				},

				hasPlaceholderSet : function() {
					return this.getTextContent() == this.textarea.element
							.getAttribute("placeholder");
				},

				isEmpty : function() {
					var innerHTML = this.element.innerHTML, elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
					return innerHTML === ""
							|| innerHTML === this.CARET_HACK
							|| this.hasPlaceholderSet()
							|| (this.getTextContent() === "" && !this.element
									.querySelector(elementsWithVisualValue));
				},

				_initSandbox : function() {
					var that = this;

					this.sandbox = new dom.Sandbox(function() {
						that._create();
					}, {
						stylesheets : this.config.stylesheets
					});
					this.iframe = this.sandbox.getIframe();

					// Create hidden field which tells the server after submit,
					// that the user used an wysiwyg editor
					var hiddenField = document.createElement("input");
					hiddenField.type = "hidden";
					hiddenField.name = "_wysihtml5_mode";
					hiddenField.value = 1;

					// Store reference to current wysihtml5 instance on the
					// textarea element
					var textareaElement = this.textarea.element;
					dom.insert(this.iframe).after(textareaElement);
					dom.insert(hiddenField).after(textareaElement);
				},

				_create : function() {
					var that = this;

					this.doc = this.sandbox.getDocument();
					this.element = this.doc.body;
					this.textarea = this.parent.textarea;
					this.element.innerHTML = this.textarea.getValue(true);
					this.enable();

					// Make sure our selection handler is ready
					this.selection = new wysihtml5.Selection(this.parent);

					// Make sure commands dispatcher is ready
					this.commands = new wysihtml5.Commands(this.parent);

					dom.copyAttributes(
							[ "className", "spellcheck", "title", "lang",
									"dir", "accessKey" ]).from(
							this.textarea.element).to(this.element);

					dom.addClass(this.element, this.config.composerClassName);

					// Make the editor look like the original textarea, by
					// syncing styles
					if (this.config.style) {
						this.style();
					}

					this.observe();

					var name = this.config.name;
					if (name) {
						dom.addClass(this.element, name);
						dom.addClass(this.iframe, name);
					}

					// Simulate html5 placeholder attribute on contentEditable
					// element
					var placeholderText = typeof (this.config.placeholder) === "string" ? this.config.placeholder
							: this.textarea.element.getAttribute("placeholder");
					if (placeholderText) {
						dom.simulatePlaceholder(this.parent, this,
								placeholderText);
					}

					// Make sure that the browser avoids using inline styles
					// whenever possible
					this.commands.exec("styleWithCSS", false);

					this._initAutoLinking();
					this._initObjectResizing();
					this._initUndoManager();

					// Simulate html5 autofocus on contentEditable element
					if (this.textarea.element.hasAttribute("autofocus")
							|| document.querySelector(":focus") == this.textarea.element) {
						setTimeout(function() {
							that.focus();
						}, 100);
					}

					wysihtml5.quirks.insertLineBreakOnReturn(this);

					// IE sometimes leaves a single paragraph, which can't be
					// removed by the user
					if (!browser.clearsContentEditableCorrectly()) {
						wysihtml5.quirks.ensureProperClearing(this);
					}

					if (!browser.clearsListsInContentEditableCorrectly()) {
						wysihtml5.quirks.ensureProperClearingOfLists(this);
					}

					// Set up a sync that makes sure that textarea and editor
					// have the same content
					if (this.initSync && this.config.sync) {
						this.initSync();
					}

					// Okay hide the textarea, we are ready to go
					this.textarea.hide();

					// Fire global (before-)load event
					this.parent.fire("beforeload").fire("load");
				},

				_initAutoLinking : function() {
					var that = this, supportsDisablingOfAutoLinking = browser
							.canDisableAutoLinking(), supportsAutoLinking = browser
							.doesAutoLinkingInContentEditable();
					if (supportsDisablingOfAutoLinking) {
						this.commands.exec("autoUrlDetect", false);
					}

					if (!this.config.autoLink) {
						return;
					}

					// Only do the auto linking by ourselves when the browser
					// doesn't support auto linking
					// OR when he supports auto linking but we were able to turn
					// it off (IE9+)
					if (!supportsAutoLinking
							|| (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
						this.parent.observe("newword:composer", function() {
							that.selection.executeAndRestore(function(
									startContainer, endContainer) {
								dom.autoLink(endContainer.parentNode);
							});
						});
					}

					// Assuming we have the following:
					// <a href="http://www.google.de">http://www.google.de</a>
					// If a user now changes the url in the innerHTML we want to
					// make sure that
					// it's synchronized with the href attribute (as long as the
					// innerHTML is still a url)
					var // Use a live NodeList to check whether there are any
					// links in the document
					links = this.sandbox.getDocument()
							.getElementsByTagName("a"),
					// The autoLink helper method reveals a reg exp to detect
					// correct urls
					urlRegExp = dom.autoLink.URL_REG_EXP, getTextContent = function(
							element) {
						var textContent = wysihtml5.lang.string(
								dom.getTextContent(element)).trim();
						if (textContent.substr(0, 4) === "www.") {
							textContent = "http://" + textContent;
						}
						return textContent;
					};

					dom
							.observe(
									this.element,
									"keydown",
									function(event) {
										if (!links.length) {
											return;
										}

										var selectedNode = that.selection
												.getSelectedNode(event.target.ownerDocument), link = dom
												.getParentElement(selectedNode,
														{
															nodeName : "A"
														}, 4), textContent;

										if (!link) {
											return;
										}

										textContent = getTextContent(link);
										// keydown is fired before the actual
										// content is changed
										// therefore we set a timeout to change
										// the href
										setTimeout(
												function() {
													var newTextContent = getTextContent(link);
													if (newTextContent === textContent) {
														return;
													}

													// Only set href when new
													// href looks like a valid
													// url
													if (newTextContent
															.match(urlRegExp)) {
														link.setAttribute(
																"href",
																newTextContent);
													}
												}, 0);
									});
				},

				_initObjectResizing : function() {
					var properties = [ "width", "height" ], propertiesLength = properties.length, element = this.element;

					this.commands.exec("enableObjectResizing",
							this.config.allowObjectResizing);

					if (this.config.allowObjectResizing) {
						// IE sets inline styles after resizing objects
						// The following lines make sure that the width/height
						// css properties
						// are copied over to the width/height attributes
						if (browser.supportsEvent("resizeend")) {
							dom
									.observe(
											element,
											"resizeend",
											function(event) {
												var target = event.target
														|| event.srcElement, style = target.style, i = 0, property;
												for (; i < propertiesLength; i++) {
													property = properties[i];
													if (style[property]) {
														target
																.setAttribute(
																		property,
																		parseInt(
																				style[property],
																				10));
														style[property] = "";
													}
												}
												// After resizing IE sometimes
												// forgets to remove the old
												// resize handles
												wysihtml5.quirks
														.redraw(element);
											});
						}
					} else {
						if (browser.supportsEvent("resizestart")) {
							dom.observe(element, "resizestart",
									function(event) {
										event.preventDefault();
									});
						}
					}
				},

				_initUndoManager : function() {
					new wysihtml5.UndoManager(this.parent);
				}
			});
})(wysihtml5);
(function(wysihtml5) {
	var dom = wysihtml5.dom, doc = document, win = window, HOST_TEMPLATE = doc
			.createElement("div"),
	/**
	 * Styles to copy from textarea to the composer element
	 */
	TEXT_FORMATTING = [ "background-color", "color", "cursor", "font-family",
			"font-size", "font-style", "font-variant", "font-weight",
			"line-height", "letter-spacing", "text-align", "text-decoration",
			"text-indent", "text-rendering", "word-break", "word-wrap",
			"word-spacing" ],
	/**
	 * Styles to copy from textarea to the iframe
	 */
	BOX_FORMATTING = [ "background-color", "border-collapse",
			"border-bottom-color", "border-bottom-style",
			"border-bottom-width", "border-left-color", "border-left-style",
			"border-left-width", "border-right-color", "border-right-style",
			"border-right-width", "border-top-color", "border-top-style",
			"border-top-width", "clear", "display", "float", "margin-bottom",
			"margin-left", "margin-right", "margin-top", "outline-color",
			"outline-offset", "outline-width", "outline-style", "padding-left",
			"padding-right", "padding-top", "padding-bottom", "position",
			"top", "left", "right", "bottom", "z-index", "vertical-align",
			"text-align", "-webkit-box-sizing", "-moz-box-sizing",
			"-ms-box-sizing", "box-sizing", "-webkit-box-shadow",
			"-moz-box-shadow", "-ms-box-shadow", "box-shadow",
			"-webkit-border-top-right-radius", "-moz-border-radius-topright",
			"border-top-right-radius", "-webkit-border-bottom-right-radius",
			"-moz-border-radius-bottomright", "border-bottom-right-radius",
			"-webkit-border-bottom-left-radius",
			"-moz-border-radius-bottomleft", "border-bottom-left-radius",
			"-webkit-border-top-left-radius", "-moz-border-radius-topleft",
			"border-top-left-radius", "width", "height" ],
	/**
	 * Styles to sync while the window gets resized
	 */
	RESIZE_STYLE = [ "width", "height", "top", "left", "right", "bottom" ], ADDITIONAL_CSS_RULES = [
			"html             { height: 100%; }",
			"body             { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",
			"._wysihtml5-temp { display: none; }",
			wysihtml5.browser.isGecko ? "body.placeholder { color: graytext !important; }"
					: "body.placeholder { color: #a9a9a9 !important; }",
			"body[disabled]   { background-color: #eee !important; color: #999 !important; cursor: default !important; }",
			// Ensure that user see's broken images and can delete them
			"img:-moz-broken  { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" ];

	/**
	 * With "setActive" IE offers a smart way of focusing elements without
	 * scrolling them into view:
	 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
	 * 
	 * Other browsers need a more hacky way: (pssst don't tell my mama) In order
	 * to prevent the element being scrolled into view when focusing it, we
	 * simply move it out of the scrollable area, focus it, and reset it's
	 * position
	 */
	var focusWithoutScrolling = function(element) {
		if (element.setActive) {
			// Following line could cause a js error when the textarea is
			// invisible
			// See https://github.com/xing/wysihtml5/issues/9
			try {
				element.setActive();
			} catch (e) {
			}
		} else {
			var elementStyle = element.style, originalScrollTop = doc.documentElement.scrollTop
					|| doc.body.scrollTop, originalScrollLeft = doc.documentElement.scrollLeft
					|| doc.body.scrollLeft, originalStyles = {
				position : elementStyle.position,
				top : elementStyle.top,
				left : elementStyle.left,
				WebkitUserSelect : elementStyle.WebkitUserSelect
			};

			dom.setStyles({
				position : "absolute",
				top : "-99999px",
				left : "-99999px",
				// Don't ask why but temporarily setting -webkit-user-select to
				// none makes the whole thing performing smoother
				WebkitUserSelect : "none"
			}).on(element);

			element.focus();

			dom.setStyles(originalStyles).on(element);

			if (win.scrollTo) {
				// Some browser extensions unset this method to prevent
				// annoyances
				// "Better PopUp Blocker" for Chrome
				// http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
				// Issue:
				// http://code.google.com/p/betterpopupblocker/issues/detail?id=1
				win.scrollTo(originalScrollLeft, originalScrollTop);
			}
		}
	};

	wysihtml5.views.Composer.prototype.style = function() {
		var that = this, originalActiveElement = doc.querySelector(":focus"), textareaElement = this.textarea.element, hasPlaceholder = textareaElement
				.hasAttribute("placeholder"), originalPlaceholder = hasPlaceholder
				&& textareaElement.getAttribute("placeholder");
		this.focusStylesHost = this.focusStylesHost
				|| HOST_TEMPLATE.cloneNode(false);
		this.blurStylesHost = this.blurStylesHost
				|| HOST_TEMPLATE.cloneNode(false);

		// Remove placeholder before copying (as the placeholder has an affect
		// on the computed style)
		if (hasPlaceholder) {
			textareaElement.removeAttribute("placeholder");
		}

		if (textareaElement === originalActiveElement) {
			textareaElement.blur();
		}

		// --------- iframe styles (has to be set before editor styles,
		// otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
		dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe)
				.andTo(this.blurStylesHost);

		// --------- editor styles ---------
		dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element)
				.andTo(this.blurStylesHost);

		// --------- apply standard rules ---------
		dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);

		// --------- :focus styles ---------
		focusWithoutScrolling(textareaElement);
		dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(
				this.focusStylesHost);
		dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(
				this.focusStylesHost);

		// Make sure that we don't change the display style of the iframe when
		// copying styles oblur/onfocus
		// this is needed for when the change_view event is fired where the
		// iframe is hidden and then
		// the blur event fires and re-displays it
		var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(
				[ "display" ]);

		// --------- restore focus ---------
		if (originalActiveElement) {
			originalActiveElement.focus();
		} else {
			textareaElement.blur();
		}

		// --------- restore placeholder ---------
		if (hasPlaceholder) {
			textareaElement.setAttribute("placeholder", originalPlaceholder);
		}

		// When copying styles, we only get the computed style which is never
		// returned in percent unit
		// Therefore we've to recalculate style onresize
		if (!wysihtml5.browser.hasCurrentStyleProperty()) {
			var winObserver = dom
					.observe(
							win,
							"resize",
							function() {
								// Remove event listener if composer doesn't
								// exist anymore
								if (!dom.contains(document.documentElement,
										that.iframe)) {
									winObserver.stop();
									return;
								}
								var originalTextareaDisplayStyle = dom
										.getStyle("display").from(
												textareaElement), originalComposerDisplayStyle = dom
										.getStyle("display").from(that.iframe);
								textareaElement.style.display = "";
								that.iframe.style.display = "none";
								dom.copyStyles(RESIZE_STYLE).from(
										textareaElement).to(that.iframe).andTo(
										that.focusStylesHost).andTo(
										that.blurStylesHost);
								that.iframe.style.display = originalComposerDisplayStyle;
								textareaElement.style.display = originalTextareaDisplayStyle;
							});
		}

		// --------- Sync focus/blur styles ---------
		this.parent.observe("focus:composer", function() {
			dom.copyStyles(boxFormattingStyles).from(that.focusStylesHost).to(
					that.iframe);
			dom.copyStyles(TEXT_FORMATTING).from(that.focusStylesHost).to(
					that.element);
		});

		this.parent.observe("blur:composer", function() {
			dom.copyStyles(boxFormattingStyles).from(that.blurStylesHost).to(
					that.iframe);
			dom.copyStyles(TEXT_FORMATTING).from(that.blurStylesHost).to(
					that.element);
		});

		return this;
	};
})(wysihtml5);
/**
 * Taking care of events - Simulating 'change' event on contentEditable element -
 * Handling drag & drop logic - Catch paste events - Dispatch proprietary
 * newword:composer event - Keyboard shortcuts
 */
(function(wysihtml5) {
	var dom = wysihtml5.dom, browser = wysihtml5.browser,
	/**
	 * Map keyCodes to query commands
	 */
	shortcuts = {
		"66" : "bold", // B
		"73" : "italic", // I
		"85" : "underline" // U
	};

	wysihtml5.views.Composer.prototype.observe = function() {
		var that = this, state = this.getValue(), iframe = this.sandbox
				.getIframe(), element = this.element, focusBlurElement = browser
				.supportsEventsInIframeCorrectly() ? element : this.sandbox
				.getWindow(),
		// Firefox < 3.5 doesn't support the drop event, instead it supports a
		// so called "dragdrop" event which behaves almost the same
		pasteEvents = browser.supportsEvent("drop") ? [ "drop", "paste" ] : [
				"dragdrop", "paste" ];

		// --------- destroy:composer event ---------
		dom.observe(iframe, "DOMNodeRemoved", function() {
			clearInterval(domNodeRemovedInterval);
			that.parent.fire("destroy:composer");
		});

		// DOMNodeRemoved event is not supported in IE 8
		var domNodeRemovedInterval = setInterval(function() {
			if (!dom.contains(document.documentElement, iframe)) {
				clearInterval(domNodeRemovedInterval);
				that.parent.fire("destroy:composer");
			}
		}, 250);

		// --------- Focus & blur logic ---------
		dom.observe(focusBlurElement, "focus", function() {
			that.parent.fire("focus").fire("focus:composer");

			// Delay storing of state until all focus handler are fired
			// especially the one which resets the placeholder
			setTimeout(function() {
				state = that.getValue();
			}, 0);
		});

		dom.observe(focusBlurElement, "blur", function() {
			if (state !== that.getValue()) {
				that.parent.fire("change").fire("change:composer");
			}
			that.parent.fire("blur").fire("blur:composer");
		});

		if (wysihtml5.browser.isIos()) {
			// When on iPad/iPhone/IPod after clicking outside of editor, the
			// editor loses focus
			// but the UI still acts as if the editor has focus (blinking caret
			// and onscreen keyboard visible)
			// We prevent that by focusing a temporary input element which
			// immediately loses focus
			dom
					.observe(
							element,
							"blur",
							function() {
								var input = element.ownerDocument
										.createElement("input"), originalScrollTop = document.documentElement.scrollTop
										|| document.body.scrollTop, originalScrollLeft = document.documentElement.scrollLeft
										|| document.body.scrollLeft;
								try {
									that.selection.insertNode(input);
								} catch (e) {
									element.appendChild(input);
								}
								input.focus();
								input.parentNode.removeChild(input);

								window.scrollTo(originalScrollLeft,
										originalScrollTop);
							});
		}

		// --------- Drag & Drop logic ---------
		dom.observe(element, "dragenter", function() {
			that.parent.fire("unset_placeholder");
		});

		if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {
			dom.observe(element, [ "dragover", "dragenter" ], function(event) {
				event.preventDefault();
			});
		}

		dom.observe(element, pasteEvents, function(event) {
			var dataTransfer = event.dataTransfer, data;

			if (dataTransfer && browser.supportsDataTransfer()) {
				data = dataTransfer.getData("text/html")
						|| dataTransfer.getData("text/plain");
			}
			if (data) {
				element.focus();
				that.commands.exec("insertHTML", data);
				that.parent.fire("paste").fire("paste:composer");
				event.stopPropagation();
				event.preventDefault();
			} else {
				setTimeout(function() {
					that.parent.fire("paste").fire("paste:composer");
				}, 0);
			}
		});

		// --------- neword event ---------
		dom.observe(element, "keyup", function(event) {
			var keyCode = event.keyCode;
			if (keyCode === wysihtml5.SPACE_KEY
					|| keyCode === wysihtml5.ENTER_KEY) {
				that.parent.fire("newword:composer");
			}
		});

		this.parent.observe("paste:composer", function() {
			setTimeout(function() {
				that.parent.fire("newword:composer");
			}, 0);
		});

		// --------- Make sure that images are selected when clicking on them
		// ---------
		if (!browser.canSelectImagesInContentEditable()) {
			dom.observe(element, "mousedown", function(event) {
				var target = event.target;
				if (target.nodeName === "IMG") {
					that.selection.selectNode(target);
					event.preventDefault();
				}
			});
		}

		// --------- Shortcut logic ---------
		dom.observe(element, "keydown", function(event) {
			var keyCode = event.keyCode, command = shortcuts[keyCode];
			if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
				that.commands.exec(command);
				event.preventDefault();
			}
		});

		// --------- Make sure that when pressing backspace/delete on selected
		// images deletes the image and it's anchor ---------
		dom
				.observe(
						element,
						"keydown",
						function(event) {
							var target = that.selection.getSelectedNode(true), keyCode = event.keyCode, parent;
							if (target
									&& target.nodeName === "IMG"
									&& (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 =>
								// backspace,
								// 46
								// =>
								// delete
								parent = target.parentNode;
								// delete the <img>
								parent.removeChild(target);
								// and it's parent <a> too if it hasn't got any
								// other child nodes
								if (parent.nodeName === "A"
										&& !parent.firstChild) {
									parent.parentNode.removeChild(parent);
								}

								setTimeout(function() {
									wysihtml5.quirks.redraw(element);
								}, 0);
								event.preventDefault();
							}
						});

		// --------- Show url in tooltip when hovering links or images ---------
		var titlePrefixes = {
			IMG : "Image: ",
			A : "Link: "
		};

		dom.observe(element, "mouseover", function(event) {
			var target = event.target, nodeName = target.nodeName, title;
			if (nodeName !== "A" && nodeName !== "IMG") {
				return;
			}
			var hasTitle = target.hasAttribute("title");
			if (!hasTitle) {
				title = titlePrefixes[nodeName]
						+ (target.getAttribute("href") || target
								.getAttribute("src"));
				target.setAttribute("title", title);
			}
		});
	};
})(wysihtml5);
/**
 * Class that takes care that the value of the composer and the textarea is
 * always in sync
 */
(function(wysihtml5) {
	var INTERVAL = 400;

	wysihtml5.views.Synchronizer = Base
			.extend(
			/** @scope wysihtml5.views.Synchronizer.prototype */
			{

				constructor : function(editor, textarea, composer) {
					this.editor = editor;
					this.textarea = textarea;
					this.composer = composer;

					this._observe();
				},

				/**
				 * Sync html from composer to textarea Takes care of
				 * placeholders
				 * 
				 * @param {Boolean}
				 *            shouldParseHtml Whether the html should be
				 *            sanitized before inserting it into the textarea
				 */
				fromComposerToTextarea : function(shouldParseHtml) {
					this.textarea.setValue(wysihtml5.lang.string(
							this.composer.getValue()).trim(), shouldParseHtml);
				},

				/**
				 * Sync value of textarea to composer Takes care of placeholders
				 * 
				 * @param {Boolean}
				 *            shouldParseHtml Whether the html should be
				 *            sanitized before inserting it into the composer
				 */
				fromTextareaToComposer : function(shouldParseHtml) {
					var textareaValue = this.textarea.getValue();
					if (textareaValue) {
						this.composer.setValue(textareaValue, shouldParseHtml);
					} else {
						this.composer.clear();
						this.editor.fire("set_placeholder");
					}
				},

				/**
				 * Invoke syncing based on view state
				 * 
				 * @param {Boolean}
				 *            shouldParseHtml Whether the html should be
				 *            sanitized before inserting it into the
				 *            composer/textarea
				 */
				sync : function(shouldParseHtml) {
					if (this.editor.currentView.name === "textarea") {
						this.fromTextareaToComposer(shouldParseHtml);
					} else {
						this.fromComposerToTextarea(shouldParseHtml);
					}
				},

				/**
				 * Initializes interval-based syncing also makes sure that
				 * on-submit the composer's content is synced with the textarea
				 * immediately when the form gets submitted
				 */
				_observe : function() {
					var interval, that = this, form = this.textarea.element.form, startInterval = function() {
						interval = setInterval(function() {
							that.fromComposerToTextarea();
						}, INTERVAL);
					}, stopInterval = function() {
						clearInterval(interval);
						interval = null;
					};

					startInterval();

					if (form) {
						// If the textarea is in a form make sure that after
						// onreset and onsubmit the composer
						// has the correct state
						wysihtml5.dom.observe(form, "submit", function() {
							that.sync(true);
						});
						wysihtml5.dom.observe(form, "reset", function() {
							setTimeout(function() {
								that.fromTextareaToComposer();
							}, 0);
						});
					}

					this.editor.observe("change_view", function(view) {
						if (view === "composer" && !interval) {
							that.fromTextareaToComposer(true);
							startInterval();
						} else if (view === "textarea") {
							that.fromComposerToTextarea(true);
							stopInterval();
						}
					});

					this.editor.observe("destroy:composer", stopInterval);
				}
			});
})(wysihtml5);
wysihtml5.views.Textarea = wysihtml5.views.View
		.extend(
		/** @scope wysihtml5.views.Textarea.prototype */
		{
			name : "textarea",

			constructor : function(parent, textareaElement, config) {
				this.base(parent, textareaElement, config);

				this._observe();
			},

			clear : function() {
				this.element.value = "";
			},

			getValue : function(parse) {
				var value = this.isEmpty() ? "" : this.element.value;
				if (parse) {
					value = this.parent.parse(value);
				}
				return value;
			},

			setValue : function(html, parse) {
				if (parse) {
					html = this.parent.parse(html);
				}
				this.element.value = html;
			},

			hasPlaceholderSet : function() {
				var supportsPlaceholder = wysihtml5.browser
						.supportsPlaceholderAttributeOn(this.element), placeholderText = this.element
						.getAttribute("placeholder")
						|| null, value = this.element.value, isEmpty = !value;
				return (supportsPlaceholder && isEmpty)
						|| (value === placeholderText);
			},

			isEmpty : function() {
				return !wysihtml5.lang.string(this.element.value).trim()
						|| this.hasPlaceholderSet();
			},

			_observe : function() {
				var element = this.element, parent = this.parent, eventMapping = {
					focusin : "focus",
					focusout : "blur"
				},
				/**
				 * Calling focus() or blur() on an element doesn't synchronously
				 * trigger the attached focus/blur events This is the case for
				 * focusin and focusout, so let's use them whenever possible,
				 * kkthxbai
				 */
				events = wysihtml5.browser.supportsEvent("focusin") ? [
						"focusin", "focusout", "change" ] : [ "focus", "blur",
						"change" ];

				parent.observe("beforeload", function() {
					wysihtml5.dom.observe(element, events, function(event) {
						var eventName = eventMapping[event.type] || event.type;
						parent.fire(eventName).fire(eventName + ":textarea");
					});

					wysihtml5.dom.observe(element, [ "paste", "drop" ],
							function() {
								setTimeout(
										function() {
											parent.fire("paste").fire(
													"paste:textarea");
										}, 0);
							});
				});
			}
		});
/**
 * Toolbar Dialog
 * 
 * @param {Element}
 *            link The toolbar link which causes the dialog to show up
 * @param {Element}
 *            container The dialog container
 * 
 * @example <!-- Toolbar link --> <a data-wysihtml5-command="insertImage">insert
 *          an image</a>
 * 
 * <!-- Dialog --> <div data-wysihtml5-dialog="insertImage" style="display:
 * none;"> <label> URL: <input data-wysihtml5-dialog-field="src"
 * value="http://"> </label> <label> Alternative text: <input
 * data-wysihtml5-dialog-field="alt" value=""> </label> </div>
 * 
 * <script> var dialog = new wysihtml5.toolbar.Dialog(
 * document.querySelector("[data-wysihtml5-command='insertImage']"),
 * document.querySelector("[data-wysihtml5-dialog='insertImage']") );
 * dialog.observe("save", function(attributes) { // do something }); </script>
 */
(function(wysihtml5) {
	var dom = wysihtml5.dom, CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened", SELECTOR_FORM_ELEMENTS = "input, select, textarea", SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]", ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";

	wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher
			.extend(
			/** @scope wysihtml5.toolbar.Dialog.prototype */
			{
				constructor : function(link, container) {
					this.link = link;
					this.container = container;
				},

				_observe : function() {
					if (this._observed) {
						return;
					}

					var that = this, callbackWrapper = function(event) {
						var attributes = that._serialize();
						if (attributes == that.elementToChange) {
							that.fire("edit", attributes);
						} else {
							that.fire("save", attributes);
						}
						that.hide();
						event.preventDefault();
						event.stopPropagation();
					};

					dom.observe(that.link, "click", function(event) {
						if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
							setTimeout(function() {
								that.hide();
							}, 0);
						}
					});

					dom.observe(this.container, "keydown", function(event) {
						var keyCode = event.keyCode;
						if (keyCode === wysihtml5.ENTER_KEY) {
							callbackWrapper(event);
						}
						if (keyCode === wysihtml5.ESCAPE_KEY) {
							that.hide();
						}
					});

					dom.delegate(this.container,
							"[data-wysihtml5-dialog-action=save]", "click",
							callbackWrapper);

					dom.delegate(this.container,
							"[data-wysihtml5-dialog-action=cancel]", "click",
							function(event) {
								that.fire("cancel");
								that.hide();
								event.preventDefault();
								event.stopPropagation();
							});

					var formElements = this.container
							.querySelectorAll(SELECTOR_FORM_ELEMENTS), i = 0, length = formElements.length, _clearInterval = function() {
						clearInterval(that.interval);
					};
					for (; i < length; i++) {
						dom.observe(formElements[i], "change", _clearInterval);
					}

					this._observed = true;
				},

				/**
				 * Grabs all fields in the dialog and puts them in key=>value
				 * style in an object which then gets returned
				 */
				_serialize : function() {
					var data = this.elementToChange || {}, fields = this.container
							.querySelectorAll(SELECTOR_FIELDS), length = fields.length, i = 0;
					for (; i < length; i++) {
						data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
					}
					return data;
				},

				/**
				 * Takes the attributes of the "elementToChange" and inserts
				 * them in their corresponding dialog input fields
				 * 
				 * Assume the "elementToChange" looks like this: <a
				 * href="http://www.google.com" target="_blank">foo</a>
				 * 
				 * and we have the following dialog: <input type="text"
				 * data-wysihtml5-dialog-field="href" value=""> <input
				 * type="text" data-wysihtml5-dialog-field="target" value="">
				 * 
				 * after calling _interpolate() the dialog will look like this
				 * <input type="text" data-wysihtml5-dialog-field="href"
				 * value="http://www.google.com"> <input type="text"
				 * data-wysihtml5-dialog-field="target" value="_blank">
				 * 
				 * Basically it adopted the attribute values into the
				 * corresponding input fields
				 * 
				 */
				_interpolate : function(avoidHiddenFields) {
					var field, fieldName, newValue, focusedElement = document
							.querySelector(":focus"), fields = this.container
							.querySelectorAll(SELECTOR_FIELDS), length = fields.length, i = 0;
					for (; i < length; i++) {
						field = fields[i];

						// Never change elements where the user is currently
						// typing in
						if (field === focusedElement) {
							continue;
						}

						// Don't update hidden fields
						// See https://github.com/xing/wysihtml5/pull/14
						if (avoidHiddenFields && field.type === "hidden") {
							continue;
						}

						fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
						newValue = this.elementToChange ? (this.elementToChange[fieldName] || "")
								: field.defaultValue;
						field.value = newValue;
					}
				},

				/**
				 * Show the dialog element
				 */
				show : function(elementToChange) {
					var that = this, firstField = this.container
							.querySelector(SELECTOR_FORM_ELEMENTS);
					this.elementToChange = elementToChange;
					this._observe();
					this._interpolate();
					if (elementToChange) {
						this.interval = setInterval(function() {
							that._interpolate(true);
						}, 500);
					}
					dom.addClass(this.link, CLASS_NAME_OPENED);
					this.container.style.display = "";
					this.fire("show");
					if (firstField && !elementToChange) {
						try {
							firstField.focus();
						} catch (e) {
						}
					}
				},

				/**
				 * Hide the dialog element
				 */
				hide : function() {
					clearInterval(this.interval);
					this.elementToChange = null;
					dom.removeClass(this.link, CLASS_NAME_OPENED);
					this.container.style.display = "none";
					this.fire("hide");
				}
			});
})(wysihtml5);
/**
 * Converts speech-to-text and inserts this into the editor As of now
 * (2011/03/25) this only is supported in Chrome >= 11
 * 
 * Note that it sends the recorded audio to the google speech recognition api:
 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
 * 
 * Current HTML5 draft can be found here
 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
 * 
 * "Accessing Google Speech API Chrome 11"
 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
 */
(function(wysihtml5) {
	var dom = wysihtml5.dom;

	var linkStyles = {
		position : "relative"
	};

	var wrapperStyles = {
		left : 0,
		margin : 0,
		opacity : 0,
		overflow : "hidden",
		padding : 0,
		position : "absolute",
		top : 0,
		zIndex : 1
	};

	var inputStyles = {
		cursor : "inherit",
		fontSize : "50px",
		height : "50px",
		marginTop : "-25px",
		outline : 0,
		padding : 0,
		position : "absolute",
		right : "-4px",
		top : "50%"
	};

	var inputAttributes = {
		"x-webkit-speech" : "",
		"speech" : ""
	};

	wysihtml5.toolbar.Speech = function(parent, link) {
		var input = document.createElement("input");
		if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
			link.style.display = "none";
			return;
		}

		var wrapper = document.createElement("div");

		wysihtml5.lang.object(wrapperStyles).merge({
			width : link.offsetWidth + "px",
			height : link.offsetHeight + "px"
		});

		dom.insert(input).into(wrapper);
		dom.insert(wrapper).into(link);

		dom.setStyles(inputStyles).on(input);
		dom.setAttributes(inputAttributes).on(input)

		dom.setStyles(wrapperStyles).on(wrapper);
		dom.setStyles(linkStyles).on(link);

		var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange"
				: "speechchange";
		dom.observe(input, eventName, function() {
			parent.execCommand("insertText", input.value);
			input.value = "";
		});

		dom.observe(input, "click", function(event) {
			if (dom.hasClass(link, "wysihtml5-command-disabled")) {
				event.preventDefault();
			}

			event.stopPropagation();
		});
	};
})(wysihtml5);
/**
 * Toolbar
 * 
 * @param {Object}
 *            parent Reference to instance of Editor instance
 * @param {Element}
 *            container Reference to the toolbar container element
 * 
 * @example <div id="toolbar"> <a data-wysihtml5-command="createLink">insert
 *          link</a> <a data-wysihtml5-command="formatBlock"
 *          data-wysihtml5-command-value="h1">insert h1</a> </div>
 * 
 * <script> var toolbar = new wysihtml5.toolbar.Toolbar(editor,
 * document.getElementById("toolbar")); </script>
 */
(function(wysihtml5) {
	var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled", CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled", CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active", CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active", dom = wysihtml5.dom;

	wysihtml5.toolbar.Toolbar = Base
			.extend(
			/** @scope wysihtml5.toolbar.Toolbar.prototype */
			{
				constructor : function(editor, container) {
					this.editor = editor;
					this.container = typeof (container) === "string" ? document
							.getElementById(container) : container;
					this.composer = editor.composer;

					this._getLinks("command");
					this._getLinks("action");

					this._observe();
					this.show();

					var speechInputLinks = this.container
							.querySelectorAll("[data-wysihtml5-command=insertSpeech]"), length = speechInputLinks.length, i = 0;
					for (; i < length; i++) {
						new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
					}
				},

				_getLinks : function(type) {
					var links = this[type + "Links"] = wysihtml5.lang.array(
							this.container.querySelectorAll("[data-wysihtml5-"
									+ type + "]")).get(), length = links.length, i = 0, mapping = this[type
							+ "Mapping"] = {}, link, group, name, value, dialog;
					for (; i < length; i++) {
						link = links[i];
						name = link.getAttribute("data-wysihtml5-" + type);
						value = link.getAttribute("data-wysihtml5-" + type
								+ "-value");
						group = this.container.querySelector("[data-wysihtml5-"
								+ type + "-group='" + name + "']");
						dialog = this._getDialog(link, name);

						mapping[name + ":" + value] = {
							link : link,
							group : group,
							name : name,
							value : value,
							dialog : dialog,
							state : false
						};
					}
				},

				_getDialog : function(link, command) {
					var that = this, dialogElement = this.container
							.querySelector("[data-wysihtml5-dialog='" + command
									+ "']"), dialog, caretBookmark;

					if (dialogElement) {
						dialog = new wysihtml5.toolbar.Dialog(link,
								dialogElement);

						dialog.observe("show", function() {
							caretBookmark = that.composer.selection
									.getBookmark();

							that.editor.fire("show:dialog", {
								command : command,
								dialogContainer : dialogElement,
								commandLink : link
							});
						});

						dialog.observe("save", function(attributes) {
							if (caretBookmark) {
								that.composer.selection
										.setBookmark(caretBookmark);
							}
							that._execCommand(command, attributes);

							that.editor.fire("save:dialog", {
								command : command,
								dialogContainer : dialogElement,
								commandLink : link
							});
						});

						dialog.observe("cancel", function() {
							that.editor.focus(false);
							that.editor.fire("cancel:dialog", {
								command : command,
								dialogContainer : dialogElement,
								commandLink : link
							});
						});
					}
					return dialog;
				},

				/**
				 * @example var toolbar = new wysihtml5.Toolbar(); // Insert a
				 *          <blockquote> element or wrap current selection in
				 *          <blockquote> toolbar.execCommand("formatBlock",
				 *          "blockquote");
				 */
				execCommand : function(command, commandValue) {
					if (this.commandsDisabled) {
						return;
					}

					var commandObj = this.commandMapping[command + ":"
							+ commandValue];

					// Show dialog when available
					if (commandObj && commandObj.dialog && !commandObj.state) {
						commandObj.dialog.show();
					} else {
						this._execCommand(command, commandValue);
					}
				},

				_execCommand : function(command, commandValue) {
					// Make sure that composer is focussed (false => don't move
					// caret to the end)
					this.editor.focus(false);

					this.composer.commands.exec(command, commandValue);
					this._updateLinkStates();
				},

				execAction : function(action) {
					var editor = this.editor;
					switch (action) {
					case "change_view":
						if (editor.currentView === editor.textarea) {
							editor.fire("change_view", "composer");
						} else {
							editor.fire("change_view", "textarea");
						}
						break;
					}
				},

				_observe : function() {
					var that = this, editor = this.editor, container = this.container, links = this.commandLinks
							.concat(this.actionLinks), length = links.length, i = 0;

					for (; i < length; i++) {
						// 'javascript:;' and unselectable=on Needed for IE, but
						// done in all browsers to make sure that all get the
						// same css applied
						// (you know, a:link { ... } doesn't match anchors with
						// missing href attribute)
						dom.setAttributes({
							href : "javascript:;",
							unselectable : "on"
						}).on(links[i]);
					}

					// Needed for opera
					dom.delegate(container, "[data-wysihtml5-command]",
							"mousedown", function(event) {
								event.preventDefault();
							});

					dom
							.delegate(
									container,
									"[data-wysihtml5-command]",
									"click",
									function(event) {
										var link = this, command = link
												.getAttribute("data-wysihtml5-command"), commandValue = link
												.getAttribute("data-wysihtml5-command-value");
										that.execCommand(command, commandValue);
										event.preventDefault();
									});

					dom.delegate(container, "[data-wysihtml5-action]", "click",
							function(event) {
								var action = this
										.getAttribute("data-wysihtml5-action");
								that.execAction(action);
								event.preventDefault();
							});

					editor.observe("focus:composer", function() {
						that.bookmark = null;
						clearInterval(that.interval);
						that.interval = setInterval(function() {
							that._updateLinkStates();
						}, 500);
					});

					editor.observe("blur:composer", function() {
						clearInterval(that.interval);
					});

					editor.observe("destroy:composer", function() {
						clearInterval(that.interval);
					});

					editor
							.observe(
									"change_view",
									function(currentView) {
										// Set timeout needed in order to let
										// the blur event fire first
										setTimeout(
												function() {
													that.commandsDisabled = (currentView !== "composer");
													that._updateLinkStates();
													if (that.commandsDisabled) {
														dom
																.addClass(
																		container,
																		CLASS_NAME_COMMANDS_DISABLED);
													} else {
														dom
																.removeClass(
																		container,
																		CLASS_NAME_COMMANDS_DISABLED);
													}
												}, 0);
									});
				},

				_updateLinkStates : function() {
					var element = this.composer.element, commandMapping = this.commandMapping, actionMapping = this.actionMapping, i, state, action, command;
					// every millisecond counts... this is executed quite often
					for (i in commandMapping) {
						command = commandMapping[i];
						if (this.commandsDisabled) {
							state = false;
							dom.removeClass(command.link,
									CLASS_NAME_COMMAND_ACTIVE);
							if (command.group) {
								dom.removeClass(command.group,
										CLASS_NAME_COMMAND_ACTIVE);
							}
							if (command.dialog) {
								command.dialog.hide();
							}
						} else {
							state = this.composer.commands.state(command.name,
									command.value);
							if (wysihtml5.lang.object(state).isArray()) {
								// Grab first and only object/element in state
								// array, otherwise convert state into boolean
								// to avoid showing a dialog for multiple
								// selected elements which may have different
								// attributes
								// eg. when two links with different href are
								// selected, the state will be an array
								// consisting of both link elements
								// but the dialog interface can only update one
								state = state.length === 1 ? state[0] : true;
							}
							dom.removeClass(command.link,
									CLASS_NAME_COMMAND_DISABLED);
							if (command.group) {
								dom.removeClass(command.group,
										CLASS_NAME_COMMAND_DISABLED);
							}
						}

						if (command.state === state) {
							continue;
						}

						command.state = state;
						if (state) {
							dom.addClass(command.link,
									CLASS_NAME_COMMAND_ACTIVE);
							if (command.group) {
								dom.addClass(command.group,
										CLASS_NAME_COMMAND_ACTIVE);
							}
							if (command.dialog) {
								if (typeof (state) === "object") {
									command.dialog.show(state);
								} else {
									command.dialog.hide();
								}
							}
						} else {
							dom.removeClass(command.link,
									CLASS_NAME_COMMAND_ACTIVE);
							if (command.group) {
								dom.removeClass(command.group,
										CLASS_NAME_COMMAND_ACTIVE);
							}
							if (command.dialog) {
								command.dialog.hide();
							}
						}
					}

					for (i in actionMapping) {
						action = actionMapping[i];

						if (action.name === "change_view") {
							action.state = this.editor.currentView === this.editor.textarea;
							if (action.state) {
								dom.addClass(action.link,
										CLASS_NAME_ACTION_ACTIVE);
							} else {
								dom.removeClass(action.link,
										CLASS_NAME_ACTION_ACTIVE);
							}
						}
					}
				},

				show : function() {
					this.container.style.display = "";
				},

				hide : function() {
					this.container.style.display = "none";
				}
			});

})(wysihtml5);
/**
 * WYSIHTML5 Editor
 * 
 * @param {Element}
 *            textareaElement Reference to the textarea which should be turned
 *            into a rich text interface
 * @param {Object}
 *            [config] See defaultConfig object below for explanation of each
 *            individual config option
 * 
 * @events load beforeload (for internal use only) focus focus:composer
 *         focus:textarea blur blur:composer blur:textarea change
 *         change:composer change:textarea paste paste:composer paste:textarea
 *         newword:composer destroy:composer undo:composer redo:composer
 *         beforecommand:composer aftercommand:composer change_view
 */
(function(wysihtml5) {
	var undef;

	var defaultConfig = {
		// Give the editor a name, the name will also be set as class name on
		// the iframe and on the iframe's body
		name : undef,
		// Whether the editor should look like the textarea (by adopting styles)
		style : true,
		// Id of the toolbar element, pass falsey value if you don't want any
		// toolbar logic
		toolbar : undef,
		// Whether urls, entered by the user should automatically become
		// clickable-links
		autoLink : true,
		// Object which includes parser rules to apply when html gets inserted
		// via copy & paste
		// See parser_rules/*.js for examples
		parserRules : {
			tags : {
				br : {},
				span : {},
				div : {},
				p : {}
			},
			classes : {}
		},
		// Parser method to use when the user inserts content via copy & paste
		parser : wysihtml5.dom.parse,
		// Class name which should be set on the contentEditable element in the
		// created sandbox iframe, can be styled via the 'stylesheets' option
		composerClassName : "wysihtml5-editor",
		// Class name to add to the body when the wysihtml5 editor is supported
		bodyClassName : "wysihtml5-supported",
		// Array (or single string) of stylesheet urls to be loaded in the
		// editor's iframe
		stylesheets : [],
		// Placeholder text to use, defaults to the placeholder attribute on the
		// textarea element
		placeholderText : undef,
		// Whether the composer should allow the user to manually resize images,
		// tables etc.
		allowObjectResizing : true,
		// Whether the rich text editor should be rendered on touch devices
		// (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
		supportTouchDevices : true
	};

	wysihtml5.Editor = wysihtml5.lang.Dispatcher
			.extend(
			/** @scope wysihtml5.Editor.prototype */
			{
				constructor : function(textareaElement, config) {
					this.textareaElement = typeof (textareaElement) === "string" ? document
							.getElementById(textareaElement)
							: textareaElement;
					this.config = wysihtml5.lang.object({})
							.merge(defaultConfig).merge(config).get();
					this.textarea = new wysihtml5.views.Textarea(this,
							this.textareaElement, this.config);
					this.currentView = this.textarea;
					this._isCompatible = wysihtml5.browser.supported();

					// Sort out unsupported/unwanted browsers here
					if (!this._isCompatible
							|| (!this.config.supportTouchDevices && wysihtml5.browser
									.isTouchDevice())) {
						var that = this;
						setTimeout(function() {
							that.fire("beforeload").fire("load");
						}, 0);
						return;
					}

					// Add class name to body, to indicate that the editor is
					// supported
					wysihtml5.dom.addClass(document.body,
							this.config.bodyClassName);

					this.composer = new wysihtml5.views.Composer(this,
							this.textareaElement, this.config);
					this.currentView = this.composer;

					if (typeof (this.config.parser) === "function") {
						this._initParser();
					}

					this.observe("beforeload", function() {
						this.synchronizer = new wysihtml5.views.Synchronizer(
								this, this.textarea, this.composer);
						if (this.config.toolbar) {
							this.toolbar = new wysihtml5.toolbar.Toolbar(this,
									this.config.toolbar);
						}
					});

					try {
						console
								.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
					} catch (e) {
					}
				},

				isCompatible : function() {
					return this._isCompatible;
				},

				clear : function() {
					this.currentView.clear();
					return this;
				},

				getValue : function(parse) {
					return this.currentView.getValue(parse);
				},

				setValue : function(html, parse) {
					if (!html) {
						return this.clear();
					}
					this.currentView.setValue(html, parse);
					return this;
				},

				focus : function(setToEnd) {
					this.currentView.focus(setToEnd);
					return this;
				},

				/**
				 * Deactivate editor (make it readonly)
				 */
				disable : function() {
					this.currentView.disable();
					return this;
				},

				/**
				 * Activate editor
				 */
				enable : function() {
					this.currentView.enable();
					return this;
				},

				isEmpty : function() {
					return this.currentView.isEmpty();
				},

				hasPlaceholderSet : function() {
					return this.currentView.hasPlaceholderSet();
				},

				parse : function(htmlOrElement) {
					var returnValue = this.config.parser(htmlOrElement,
							this.config.parserRules, this.composer.sandbox
									.getDocument(), true);
					if (typeof (htmlOrElement) === "object") {
						wysihtml5.quirks.redraw(htmlOrElement);
					}
					return returnValue;
				},

				/**
				 * Prepare html parser logic - Observes for paste and drop
				 */
				_initParser : function() {
					this.observe("paste:composer", function() {
						var keepScrollPosition = true, that = this;
						that.composer.selection.executeAndRestore(function() {
							wysihtml5.quirks
									.cleanPastedHTML(that.composer.element);
							that.parse(that.composer.element);
						}, keepScrollPosition);
					});

					this.observe("paste:textarea", function() {
						var value = this.textarea.getValue(), newValue;
						newValue = this.parse(value);
						this.textarea.setValue(newValue);
					});
				}
			});
})(wysihtml5);